From fd5389e0d7e72ff176143496261f39b8dcd307fe Mon Sep 17 00:00:00 2001 From: Pete Date: Mon, 19 Aug 2013 14:05:01 +0100 Subject: [PATCH 01/80] Moved trovebox.api and trovebox.objects into packages --- tests/unit/test_albums.py | 2 +- tests/unit/test_http.py | 6 +- tests/unit/test_photos.py | 2 +- tests/unit/test_tags.py | 2 +- trovebox/__init__.py | 6 +- trovebox/api/__init__.py | 4 + trovebox/{ => api}/api_album.py | 4 +- trovebox/{ => api}/api_photo.py | 8 +- trovebox/{ => api}/api_tag.py | 4 +- trovebox/http.py | 2 +- trovebox/objects.py | 235 ---------------------------- trovebox/objects/__init__.py | 4 + trovebox/objects/album.py | 73 +++++++++ trovebox/objects/photo.py | 96 ++++++++++++ trovebox/objects/tag.py | 31 ++++ trovebox/objects/trovebox_object.py | 50 ++++++ 16 files changed, 279 insertions(+), 250 deletions(-) create mode 100644 trovebox/api/__init__.py rename trovebox/{ => api}/api_album.py (92%) rename trovebox/{ => api}/api_photo.py (95%) rename trovebox/{ => api}/api_tag.py (89%) delete mode 100644 trovebox/objects.py create mode 100644 trovebox/objects/__init__.py create mode 100644 trovebox/objects/album.py create mode 100644 trovebox/objects/photo.py create mode 100644 trovebox/objects/tag.py create mode 100644 trovebox/objects/trovebox_object.py diff --git a/tests/unit/test_albums.py b/tests/unit/test_albums.py index c01e441..db91e31 100644 --- a/tests/unit/test_albums.py +++ b/tests/unit/test_albums.py @@ -19,7 +19,7 @@ class TestAlbums(unittest.TestCase): "totalRows": 2}] def setUp(self): self.client = trovebox.Trovebox(host=self.test_host) - self.test_albums = [trovebox.objects.Album(self.client, album) + self.test_albums = [trovebox.objects.album.Album(self.client, album) for album in self.test_albums_dict] @staticmethod diff --git a/tests/unit/test_http.py b/tests/unit/test_http.py index 98883f1..bf34f3c 100644 --- a/tests/unit/test_http.py +++ b/tests/unit/test_http.py @@ -171,9 +171,9 @@ class TestHttp(unittest.TestCase): def test_get_parameter_processing(self): """Check that the parameter processing function is working""" self._register_uri(httpretty.GET) - photo = trovebox.objects.Photo(None, {"id": "photo_id"}) - album = trovebox.objects.Album(None, {"id": "album_id"}) - tag = trovebox.objects.Tag(None, {"id": "tag_id"}) + photo = trovebox.objects.photo.Photo(None, {"id": "photo_id"}) + album = trovebox.objects.photo.Album(None, {"id": "album_id"}) + tag = trovebox.objects.tag.Tag(None, {"id": "tag_id"}) self.client.get(self.test_endpoint, photo=photo, album=album, tag=tag, list_=[photo, album, tag], diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index a1533c6..def92f0 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -18,7 +18,7 @@ class TestPhotos(unittest.TestCase): "totalPages": 1, "totalRows": 2}] def setUp(self): self.client = trovebox.Trovebox(host=self.test_host) - self.test_photos = [trovebox.objects.Photo(self.client, photo) + self.test_photos = [trovebox.objects.photo.Photo(self.client, photo) for photo in self.test_photos_dict] @staticmethod diff --git a/tests/unit/test_tags.py b/tests/unit/test_tags.py index c0248cd..bc883fb 100644 --- a/tests/unit/test_tags.py +++ b/tests/unit/test_tags.py @@ -15,7 +15,7 @@ class TestTags(unittest.TestCase): def setUp(self): self.client = trovebox.Trovebox(host=self.test_host) - self.test_tags = [trovebox.objects.Tag(self.client, tag) + self.test_tags = [trovebox.objects.tag.Tag(self.client, tag) for tag in self.test_tags_dict] @staticmethod diff --git a/trovebox/__init__.py b/trovebox/__init__.py index f8b0a28..18e1711 100644 --- a/trovebox/__init__.py +++ b/trovebox/__init__.py @@ -4,9 +4,9 @@ __init__.py : Trovebox package top level from .http import Http from .errors import * from ._version import __version__ -from . import api_photo -from . import api_tag -from . import api_album +from api import api_photo +from api import api_tag +from api import api_album LATEST_API_VERSION = 2 diff --git a/trovebox/api/__init__.py b/trovebox/api/__init__.py new file mode 100644 index 0000000..5dd3c68 --- /dev/null +++ b/trovebox/api/__init__.py @@ -0,0 +1,4 @@ +""" +trovebox.api Package + Definitions for each of the Trovebox API endpoints +""" diff --git a/trovebox/api_album.py b/trovebox/api/api_album.py similarity index 92% rename from trovebox/api_album.py rename to trovebox/api/api_album.py index 4b69f87..bbf50e6 100644 --- a/trovebox/api_album.py +++ b/trovebox/api/api_album.py @@ -1,9 +1,10 @@ """ api_album.py : Trovebox Album API Classes """ -from .objects import Album +from trovebox.objects.album import Album class ApiAlbums(object): + """ Definitions of /albums/ API endpoints """ def __init__(self, client): self._client = client @@ -13,6 +14,7 @@ class ApiAlbums(object): return [Album(self._client, album) for album in results] class ApiAlbum(object): + """ Definitions of /album/ API endpoints """ def __init__(self, client): self._client = client diff --git a/trovebox/api_photo.py b/trovebox/api/api_photo.py similarity index 95% rename from trovebox/api_photo.py rename to trovebox/api/api_photo.py index 702a3de..0575e5c 100644 --- a/trovebox/api_photo.py +++ b/trovebox/api/api_photo.py @@ -3,9 +3,9 @@ api_photo.py : Trovebox Photo API Classes """ import base64 -from .errors import TroveboxError -from . import http -from .objects import Photo +from trovebox.errors import TroveboxError +from trovebox import http +from trovebox.objects.photo import Photo def extract_ids(photos): """ @@ -21,6 +21,7 @@ def extract_ids(photos): return ids class ApiPhotos(object): + """ Definitions of /photos/ API endpoints """ def __init__(self, client): self._client = client @@ -55,6 +56,7 @@ class ApiPhotos(object): return True class ApiPhoto(object): + """ Definitions of /photo/ API endpoints """ def __init__(self, client): self._client = client diff --git a/trovebox/api_tag.py b/trovebox/api/api_tag.py similarity index 89% rename from trovebox/api_tag.py rename to trovebox/api/api_tag.py index 0a694a6..56e5e8b 100644 --- a/trovebox/api_tag.py +++ b/trovebox/api/api_tag.py @@ -1,9 +1,10 @@ """ api_tag.py : Trovebox Tag API Classes """ -from .objects import Tag +from trovebox.objects.tag import Tag class ApiTags(object): + """ Definitions of /tags/ API endpoints """ def __init__(self, client): self._client = client @@ -13,6 +14,7 @@ class ApiTags(object): return [Tag(self._client, tag) for tag in results] class ApiTag(object): + """ Definitions of /tag/ API endpoints """ def __init__(self, client): self._client = client diff --git a/trovebox/http.py b/trovebox/http.py index 4cc0faf..2cd6191 100644 --- a/trovebox/http.py +++ b/trovebox/http.py @@ -11,7 +11,7 @@ try: except ImportError: from urlparse import urlparse, urlunparse # Python2 -from .objects import TroveboxObject +from objects.trovebox_object import TroveboxObject from .errors import * from .auth import Auth diff --git a/trovebox/objects.py b/trovebox/objects.py deleted file mode 100644 index 7c1335a..0000000 --- a/trovebox/objects.py +++ /dev/null @@ -1,235 +0,0 @@ -""" -objects.py : Basic Trovebox API Objects -""" -try: - from urllib.parse import quote # Python3 -except ImportError: - from urllib import quote # Python2 - -from .errors import TroveboxError - -class TroveboxObject(object): - """ Base object supporting the storage of custom fields as attributes """ - def __init__(self, trovebox, json_dict): - self.id = None - self.name = None - self._trovebox = trovebox - self._json_dict = json_dict - self._set_fields(json_dict) - - def _set_fields(self, json_dict): - """ Set this object's attributes specified in json_dict """ - for key, value in json_dict.items(): - if key.startswith("_"): - raise ValueError("Illegal attribute: %s" % key) - setattr(self, key, value) - - def _replace_fields(self, json_dict): - """ - Delete this object's attributes, and replace with - those in json_dict. - """ - for key in self._json_dict.keys(): - delattr(self, key) - self._json_dict = json_dict - self._set_fields(json_dict) - - def _delete_fields(self): - """ - Delete this object's attributes, including name and id - """ - for key in self._json_dict.keys(): - delattr(self, key) - self._json_dict = {} - self.id = None - self.name = None - - def __repr__(self): - if self.name is not None: - return "<%s name='%s'>" % (self.__class__, self.name) - elif self.id is not None: - return "<%s id='%s'>" % (self.__class__, self.id) - else: - return "<%s>" % (self.__class__) - - def get_fields(self): - """ Returns this object's attributes """ - return self._json_dict - - -class Photo(TroveboxObject): - def delete(self, **kwds): - """ - Delete this photo. - Returns True if successful. - Raises an TroveboxError if not. - """ - result = self._trovebox.post("/photo/%s/delete.json" % - self.id, **kwds)["result"] - if not result: - raise TroveboxError("Delete response returned False") - self._delete_fields() - return result - - def edit(self, **kwds): - """ Returns an HTML form to edit the photo """ - result = self._trovebox.get("/photo/%s/edit.json" % - self.id, **kwds)["result"] - return result["markup"] - - def replace(self, photo_file, **kwds): - """ Not implemented yet """ - raise NotImplementedError() - - def replace_encoded(self, photo_file, **kwds): - """ Not implemented yet """ - raise NotImplementedError() - - def update(self, **kwds): - """ Update this photo with the specified parameters """ - new_dict = self._trovebox.post("/photo/%s/update.json" % - self.id, **kwds)["result"] - self._replace_fields(new_dict) - - def view(self, **kwds): - """ - Used to view the photo at a particular size. - Updates the photo's fields with the response. - """ - new_dict = self._trovebox.get("/photo/%s/view.json" % - self.id, **kwds)["result"] - self._replace_fields(new_dict) - - def dynamic_url(self, **kwds): - """ Not implemented yet """ - raise NotImplementedError() - - def next_previous(self, **kwds): - """ - Returns a dict containing the next and previous photo lists - (there may be more than one next/previous photo returned). - """ - result = self._trovebox.get("/photo/%s/nextprevious.json" % - self.id, **kwds)["result"] - value = {} - if "next" in result: - # Workaround for APIv1 - if not isinstance(result["next"], list): - result["next"] = [result["next"]] - - value["next"] = [] - for photo in result["next"]: - value["next"].append(Photo(self._trovebox, photo)) - - if "previous" in result: - # Workaround for APIv1 - if not isinstance(result["previous"], list): - result["previous"] = [result["previous"]] - - value["previous"] = [] - for photo in result["previous"]: - value["previous"].append(Photo(self._trovebox, photo)) - - return value - - def transform(self, **kwds): - """ - Performs transformation specified in **kwds - Example: transform(rotate=90) - """ - new_dict = self._trovebox.post("/photo/%s/transform.json" % - self.id, **kwds)["result"] - - # APIv1 doesn't return the transformed photo (frontend issue #955) - if isinstance(new_dict, bool): - new_dict = self._trovebox.get("/photo/%s/view.json" % - self.id)["result"] - - self._replace_fields(new_dict) - -class Tag(TroveboxObject): - def delete(self, **kwds): - """ - Delete this tag. - Returns True if successful. - Raises an TroveboxError if not. - """ - result = self._trovebox.post("/tag/%s/delete.json" % - quote(self.id), **kwds)["result"] - if not result: - raise TroveboxError("Delete response returned False") - self._delete_fields() - return result - - def update(self, **kwds): - """ Update this tag with the specified parameters """ - new_dict = self._trovebox.post("/tag/%s/update.json" % quote(self.id), - **kwds)["result"] - self._replace_fields(new_dict) - - -class Album(TroveboxObject): - def __init__(self, trovebox, json_dict): - self.photos = None - self.cover = None - TroveboxObject.__init__(self, trovebox, json_dict) - self._update_fields_with_objects() - - def _update_fields_with_objects(self): - """ Convert dict fields into objects, where appropriate """ - # Update the cover with a photo object - if isinstance(self.cover, dict): - self.cover = Photo(self._trovebox, self.cover) - # Update the photo list with photo objects - if isinstance(self.photos, list): - for i, photo in enumerate(self.photos): - if isinstance(photo, dict): - self.photos[i] = Photo(self._trovebox, photo) - - def delete(self, **kwds): - """ - Delete this album. - Returns True if successful. - Raises an TroveboxError if not. - """ - result = self._trovebox.post("/album/%s/delete.json" % - self.id, **kwds)["result"] - if not result: - raise TroveboxError("Delete response returned False") - self._delete_fields() - return result - - def form(self, **kwds): - """ Not implemented yet """ - raise NotImplementedError() - - def add_photos(self, photos, **kwds): - """ Not implemented yet """ - raise NotImplementedError() - - def remove_photos(self, photos, **kwds): - """ Not implemented yet """ - raise NotImplementedError() - - def update(self, **kwds): - """ Update this album with the specified parameters """ - new_dict = self._trovebox.post("/album/%s/update.json" % - self.id, **kwds)["result"] - - # APIv1 doesn't return the updated album (frontend issue #937) - if isinstance(new_dict, bool): - new_dict = self._trovebox.get("/album/%s/view.json" % - self.id)["result"] - - self._replace_fields(new_dict) - self._update_fields_with_objects() - - def view(self, **kwds): - """ - Requests the full contents of the album. - Updates the album's fields with the response. - """ - result = self._trovebox.get("/album/%s/view.json" % - self.id, **kwds)["result"] - self._replace_fields(result) - self._update_fields_with_objects() diff --git a/trovebox/objects/__init__.py b/trovebox/objects/__init__.py new file mode 100644 index 0000000..a64f448 --- /dev/null +++ b/trovebox/objects/__init__.py @@ -0,0 +1,4 @@ +""" +trovebox.objects Package + Object classes returned by the API. +""" diff --git a/trovebox/objects/album.py b/trovebox/objects/album.py new file mode 100644 index 0000000..40ff178 --- /dev/null +++ b/trovebox/objects/album.py @@ -0,0 +1,73 @@ +""" +Representation of an Album object +""" +from trovebox.errors import TroveboxError +from .trovebox_object import TroveboxObject +from .photo import Photo + +class Album(TroveboxObject): + """ Representation of an Album object """ + def __init__(self, trovebox, json_dict): + self.photos = None + self.cover = None + TroveboxObject.__init__(self, trovebox, json_dict) + self._update_fields_with_objects() + + def _update_fields_with_objects(self): + """ Convert dict fields into objects, where appropriate """ + # Update the cover with a photo object + if isinstance(self.cover, dict): + self.cover = Photo(self._trovebox, self.cover) + # Update the photo list with photo objects + if isinstance(self.photos, list): + for i, photo in enumerate(self.photos): + if isinstance(photo, dict): + self.photos[i] = Photo(self._trovebox, photo) + + def delete(self, **kwds): + """ + Delete this album. + Returns True if successful. + Raises an TroveboxError if not. + """ + result = self._trovebox.post("/album/%s/delete.json" % + self.id, **kwds)["result"] + if not result: + raise TroveboxError("Delete response returned False") + self._delete_fields() + return result + + def form(self, **kwds): + """ Not implemented yet """ + raise NotImplementedError() + + def add_photos(self, photos, **kwds): + """ Not implemented yet """ + raise NotImplementedError() + + def remove_photos(self, photos, **kwds): + """ Not implemented yet """ + raise NotImplementedError() + + def update(self, **kwds): + """ Update this album with the specified parameters """ + new_dict = self._trovebox.post("/album/%s/update.json" % + self.id, **kwds)["result"] + + # APIv1 doesn't return the updated album (frontend issue #937) + if isinstance(new_dict, bool): + new_dict = self._trovebox.get("/album/%s/view.json" % + self.id)["result"] + + self._replace_fields(new_dict) + self._update_fields_with_objects() + + def view(self, **kwds): + """ + Requests the full contents of the album. + Updates the album's fields with the response. + """ + result = self._trovebox.get("/album/%s/view.json" % + self.id, **kwds)["result"] + self._replace_fields(result) + self._update_fields_with_objects() diff --git a/trovebox/objects/photo.py b/trovebox/objects/photo.py new file mode 100644 index 0000000..d715a4f --- /dev/null +++ b/trovebox/objects/photo.py @@ -0,0 +1,96 @@ +""" +Representation of a Photo object +""" +from trovebox.errors import TroveboxError +from .trovebox_object import TroveboxObject + +class Photo(TroveboxObject): + """ Representation of a Photo object """ + def delete(self, **kwds): + """ + Delete this photo. + Returns True if successful. + Raises an TroveboxError if not. + """ + result = self._trovebox.post("/photo/%s/delete.json" % + self.id, **kwds)["result"] + if not result: + raise TroveboxError("Delete response returned False") + self._delete_fields() + return result + + def edit(self, **kwds): + """ Returns an HTML form to edit the photo """ + result = self._trovebox.get("/photo/%s/edit.json" % + self.id, **kwds)["result"] + return result["markup"] + + def replace(self, photo_file, **kwds): + """ Not implemented yet """ + raise NotImplementedError() + + def replace_encoded(self, photo_file, **kwds): + """ Not implemented yet """ + raise NotImplementedError() + + def update(self, **kwds): + """ Update this photo with the specified parameters """ + new_dict = self._trovebox.post("/photo/%s/update.json" % + self.id, **kwds)["result"] + self._replace_fields(new_dict) + + def view(self, **kwds): + """ + Used to view the photo at a particular size. + Updates the photo's fields with the response. + """ + new_dict = self._trovebox.get("/photo/%s/view.json" % + self.id, **kwds)["result"] + self._replace_fields(new_dict) + + def dynamic_url(self, **kwds): + """ Not implemented yet """ + raise NotImplementedError() + + def next_previous(self, **kwds): + """ + Returns a dict containing the next and previous photo lists + (there may be more than one next/previous photo returned). + """ + result = self._trovebox.get("/photo/%s/nextprevious.json" % + self.id, **kwds)["result"] + value = {} + if "next" in result: + # Workaround for APIv1 + if not isinstance(result["next"], list): + result["next"] = [result["next"]] + + value["next"] = [] + for photo in result["next"]: + value["next"].append(Photo(self._trovebox, photo)) + + if "previous" in result: + # Workaround for APIv1 + if not isinstance(result["previous"], list): + result["previous"] = [result["previous"]] + + value["previous"] = [] + for photo in result["previous"]: + value["previous"].append(Photo(self._trovebox, photo)) + + return value + + def transform(self, **kwds): + """ + Performs transformation specified in **kwds + Example: transform(rotate=90) + """ + new_dict = self._trovebox.post("/photo/%s/transform.json" % + self.id, **kwds)["result"] + + # APIv1 doesn't return the transformed photo (frontend issue #955) + if isinstance(new_dict, bool): + new_dict = self._trovebox.get("/photo/%s/view.json" % + self.id)["result"] + + self._replace_fields(new_dict) diff --git a/trovebox/objects/tag.py b/trovebox/objects/tag.py new file mode 100644 index 0000000..eb3adce --- /dev/null +++ b/trovebox/objects/tag.py @@ -0,0 +1,31 @@ +""" +Representation of a Tag object +""" +try: + from urllib.parse import quote # Python3 +except ImportError: + from urllib import quote # Python2 + +from trovebox.errors import TroveboxError +from .trovebox_object import TroveboxObject + +class Tag(TroveboxObject): + """ Representation of a Tag object """ + def delete(self, **kwds): + """ + Delete this tag. + Returns True if successful. + Raises an TroveboxError if not. + """ + result = self._trovebox.post("/tag/%s/delete.json" % + quote(self.id), **kwds)["result"] + if not result: + raise TroveboxError("Delete response returned False") + self._delete_fields() + return result + + def update(self, **kwds): + """ Update this tag with the specified parameters """ + new_dict = self._trovebox.post("/tag/%s/update.json" % quote(self.id), + **kwds)["result"] + self._replace_fields(new_dict) diff --git a/trovebox/objects/trovebox_object.py b/trovebox/objects/trovebox_object.py new file mode 100644 index 0000000..49122ec --- /dev/null +++ b/trovebox/objects/trovebox_object.py @@ -0,0 +1,50 @@ +""" +Base object supporting the storage of custom fields as attributes +""" +class TroveboxObject(object): + """ Base object supporting the storage of custom fields as attributes """ + def __init__(self, trovebox, json_dict): + self.id = None + self.name = None + self._trovebox = trovebox + self._json_dict = json_dict + self._set_fields(json_dict) + + def _set_fields(self, json_dict): + """ Set this object's attributes specified in json_dict """ + for key, value in json_dict.items(): + if key.startswith("_"): + raise ValueError("Illegal attribute: %s" % key) + setattr(self, key, value) + + def _replace_fields(self, json_dict): + """ + Delete this object's attributes, and replace with + those in json_dict. + """ + for key in self._json_dict.keys(): + delattr(self, key) + self._json_dict = json_dict + self._set_fields(json_dict) + + def _delete_fields(self): + """ + Delete this object's attributes, including name and id + """ + for key in self._json_dict.keys(): + delattr(self, key) + self._json_dict = {} + self.id = None + self.name = None + + def __repr__(self): + if self.name is not None: + return "<%s name='%s'>" % (self.__class__, self.name) + elif self.id is not None: + return "<%s id='%s'>" % (self.__class__, self.id) + else: + return "<%s>" % (self.__class__) + + def get_fields(self): + """ Returns this object's attributes """ + return self._json_dict From 06362721f04c62a1bda3264b611e7130c7466715 Mon Sep 17 00:00:00 2001 From: Pete Date: Mon, 19 Aug 2013 14:10:15 +0100 Subject: [PATCH 02/80] Comment fixes: "an Trovebox" -> "a Trovebox" --- trovebox/api/api_album.py | 2 +- trovebox/api/api_photo.py | 2 +- trovebox/api/api_tag.py | 2 +- trovebox/errors.py | 2 +- trovebox/http.py | 2 +- trovebox/objects/album.py | 2 +- trovebox/objects/photo.py | 2 +- trovebox/objects/tag.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/trovebox/api/api_album.py b/trovebox/api/api_album.py index bbf50e6..2767c3f 100644 --- a/trovebox/api/api_album.py +++ b/trovebox/api/api_album.py @@ -28,7 +28,7 @@ class ApiAlbum(object): """ Delete an album. Returns True if successful. - Raises an TroveboxError if not. + Raises a TroveboxError if not. """ if not isinstance(album, Album): album = Album(self._client, {"id": album}) diff --git a/trovebox/api/api_photo.py b/trovebox/api/api_photo.py index 0575e5c..ee835e4 100644 --- a/trovebox/api/api_photo.py +++ b/trovebox/api/api_photo.py @@ -64,7 +64,7 @@ class ApiPhoto(object): """ Delete a photo. Returns True if successful. - Raises an TroveboxError if not. + Raises a TroveboxError if not. """ if not isinstance(photo, Photo): photo = Photo(self._client, {"id": photo}) diff --git a/trovebox/api/api_tag.py b/trovebox/api/api_tag.py index 56e5e8b..e46afda 100644 --- a/trovebox/api/api_tag.py +++ b/trovebox/api/api_tag.py @@ -29,7 +29,7 @@ class ApiTag(object): """ Delete a tag. Returns True if successful. - Raises an TroveboxError if not. + Raises a TroveboxError if not. """ if not isinstance(tag, Tag): tag = Tag(self._client, {"id": tag}) diff --git a/trovebox/errors.py b/trovebox/errors.py index 15b13cc..7d225e0 100644 --- a/trovebox/errors.py +++ b/trovebox/errors.py @@ -2,7 +2,7 @@ errors.py : Trovebox Error Classes """ class TroveboxError(Exception): - """ Indicates that an Trovebox operation failed """ + """ Indicates that a Trovebox operation failed """ pass class TroveboxDuplicateError(TroveboxError): diff --git a/trovebox/http.py b/trovebox/http.py index 2cd6191..54222ef 100644 --- a/trovebox/http.py +++ b/trovebox/http.py @@ -25,7 +25,7 @@ DUPLICATE_RESPONSE = {"code": 409, class Http(object): """ - Base class to handle HTTP requests to an Trovebox server. + Base class to handle HTTP requests to a Trovebox server. If no parameters are specified, auth config is loaded from the default location (~/.config/trovebox/default). The config_file parameter is used to specify an alternate config file. diff --git a/trovebox/objects/album.py b/trovebox/objects/album.py index 40ff178..d064e52 100644 --- a/trovebox/objects/album.py +++ b/trovebox/objects/album.py @@ -28,7 +28,7 @@ class Album(TroveboxObject): """ Delete this album. Returns True if successful. - Raises an TroveboxError if not. + Raises a TroveboxError if not. """ result = self._trovebox.post("/album/%s/delete.json" % self.id, **kwds)["result"] diff --git a/trovebox/objects/photo.py b/trovebox/objects/photo.py index d715a4f..f036d8b 100644 --- a/trovebox/objects/photo.py +++ b/trovebox/objects/photo.py @@ -10,7 +10,7 @@ class Photo(TroveboxObject): """ Delete this photo. Returns True if successful. - Raises an TroveboxError if not. + Raises a TroveboxError if not. """ result = self._trovebox.post("/photo/%s/delete.json" % self.id, **kwds)["result"] diff --git a/trovebox/objects/tag.py b/trovebox/objects/tag.py index eb3adce..5ee1280 100644 --- a/trovebox/objects/tag.py +++ b/trovebox/objects/tag.py @@ -15,7 +15,7 @@ class Tag(TroveboxObject): """ Delete this tag. Returns True if successful. - Raises an TroveboxError if not. + Raises a TroveboxError if not. """ result = self._trovebox.post("/tag/%s/delete.json" % quote(self.id), **kwds)["result"] From 6867ba0dbab9985ad52c1b7063c5a58763978986 Mon Sep 17 00:00:00 2001 From: Pete Date: Mon, 19 Aug 2013 14:15:05 +0100 Subject: [PATCH 03/80] Consistency improvement: new_dict -> result --- trovebox/objects/album.py | 12 ++++++------ trovebox/objects/photo.py | 24 ++++++++++++------------ trovebox/objects/tag.py | 6 +++--- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/trovebox/objects/album.py b/trovebox/objects/album.py index d064e52..853370c 100644 --- a/trovebox/objects/album.py +++ b/trovebox/objects/album.py @@ -51,15 +51,15 @@ class Album(TroveboxObject): def update(self, **kwds): """ Update this album with the specified parameters """ - new_dict = self._trovebox.post("/album/%s/update.json" % - self.id, **kwds)["result"] + result = self._trovebox.post("/album/%s/update.json" % + self.id, **kwds)["result"] # APIv1 doesn't return the updated album (frontend issue #937) - if isinstance(new_dict, bool): - new_dict = self._trovebox.get("/album/%s/view.json" % - self.id)["result"] + if isinstance(result, bool): + result = self._trovebox.get("/album/%s/view.json" % + self.id)["result"] - self._replace_fields(new_dict) + self._replace_fields(result) self._update_fields_with_objects() def view(self, **kwds): diff --git a/trovebox/objects/photo.py b/trovebox/objects/photo.py index f036d8b..a5b3f4e 100644 --- a/trovebox/objects/photo.py +++ b/trovebox/objects/photo.py @@ -35,18 +35,18 @@ class Photo(TroveboxObject): def update(self, **kwds): """ Update this photo with the specified parameters """ - new_dict = self._trovebox.post("/photo/%s/update.json" % - self.id, **kwds)["result"] - self._replace_fields(new_dict) + result = self._trovebox.post("/photo/%s/update.json" % + self.id, **kwds)["result"] + self._replace_fields(result) def view(self, **kwds): """ Used to view the photo at a particular size. Updates the photo's fields with the response. """ - new_dict = self._trovebox.get("/photo/%s/view.json" % - self.id, **kwds)["result"] - self._replace_fields(new_dict) + result = self._trovebox.get("/photo/%s/view.json" % + self.id, **kwds)["result"] + self._replace_fields(result) def dynamic_url(self, **kwds): """ Not implemented yet """ @@ -85,12 +85,12 @@ class Photo(TroveboxObject): Performs transformation specified in **kwds Example: transform(rotate=90) """ - new_dict = self._trovebox.post("/photo/%s/transform.json" % - self.id, **kwds)["result"] + result = self._trovebox.post("/photo/%s/transform.json" % + self.id, **kwds)["result"] # APIv1 doesn't return the transformed photo (frontend issue #955) - if isinstance(new_dict, bool): - new_dict = self._trovebox.get("/photo/%s/view.json" % - self.id)["result"] + if isinstance(result, bool): + result = self._trovebox.get("/photo/%s/view.json" % + self.id)["result"] - self._replace_fields(new_dict) + self._replace_fields(result) diff --git a/trovebox/objects/tag.py b/trovebox/objects/tag.py index 5ee1280..94d5ac4 100644 --- a/trovebox/objects/tag.py +++ b/trovebox/objects/tag.py @@ -26,6 +26,6 @@ class Tag(TroveboxObject): def update(self, **kwds): """ Update this tag with the specified parameters """ - new_dict = self._trovebox.post("/tag/%s/update.json" % quote(self.id), - **kwds)["result"] - self._replace_fields(new_dict) + result = self._trovebox.post("/tag/%s/update.json" % quote(self.id), + **kwds)["result"] + self._replace_fields(result) From 90608ca49a7afdc6b94b2fc452ca19b69a458f42 Mon Sep 17 00:00:00 2001 From: Pete Date: Mon, 19 Aug 2013 16:11:40 +0100 Subject: [PATCH 04/80] Pylint fixes --- trovebox/.pylint-ignores.patch | 141 ++++++++++----------------------- trovebox/__init__.py | 8 +- trovebox/http.py | 2 +- 3 files changed, 47 insertions(+), 104 deletions(-) diff --git a/trovebox/.pylint-ignores.patch b/trovebox/.pylint-ignores.patch index 41fca5e..3aa911d 100644 --- a/trovebox/.pylint-ignores.patch +++ b/trovebox/.pylint-ignores.patch @@ -1,69 +1,30 @@ -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api_album.py patched/api_album.py ---- original/api_album.py 2013-08-16 18:12:30.434212000 +0100 -+++ patched/api_album.py 2013-08-16 18:13:29.678506001 +0100 +diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_album.py patched/api/api_album.py +--- original/api/api_album.py 2013-08-19 16:08:00.231047000 +0100 ++++ patched/api/api_album.py 2013-08-19 16:09:30.263494209 +0100 @@ -3,7 +3,7 @@ """ - from .objects import Album + from trovebox.objects.album import Album -class ApiAlbums(object): -+class ApiAlbums(object): # pylint: disable=R0903,C0111 ++class ApiAlbums(object): # pylint: disable=R0903 + """ Definitions of /albums/ API endpoints """ def __init__(self, client): self._client = client - -@@ -12,7 +12,7 @@ - results = self._client.get("/albums/list.json", **kwds)["result"] - return [Album(self._client, album) for album in results] - --class ApiAlbum(object): -+class ApiAlbum(object): # pylint: disable=C0111 - def __init__(self, client): - self._client = client - -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api_photo.py patched/api_photo.py ---- original/api_photo.py 2013-08-16 18:12:30.434212000 +0100 -+++ patched/api_photo.py 2013-08-16 18:13:29.678506001 +0100 -@@ -20,7 +20,7 @@ - ids.append(photo) - return ids - --class ApiPhotos(object): -+class ApiPhotos(object): # pylint: disable=C0111 - def __init__(self, client): - self._client = client - -@@ -54,7 +54,7 @@ - raise TroveboxError("Delete response returned False") - return True - --class ApiPhoto(object): -+class ApiPhoto(object): # pylint: disable=C0111 - def __init__(self, client): - self._client = client - -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api_tag.py patched/api_tag.py ---- original/api_tag.py 2013-08-16 18:12:30.434212000 +0100 -+++ patched/api_tag.py 2013-08-16 18:13:29.678506001 +0100 +diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_tag.py patched/api/api_tag.py +--- original/api/api_tag.py 2013-08-19 16:08:00.231047000 +0100 ++++ patched/api/api_tag.py 2013-08-19 16:09:30.263494209 +0100 @@ -3,7 +3,7 @@ """ - from .objects import Tag + from trovebox.objects.tag import Tag -class ApiTags(object): -+class ApiTags(object): # pylint: disable=R0903,C0111 ++class ApiTags(object): # pylint: disable=R0903 + """ Definitions of /tags/ API endpoints """ def __init__(self, client): self._client = client - -@@ -12,7 +12,7 @@ - results = self._client.get("/tags/list.json", **kwds)["result"] - return [Tag(self._client, tag) for tag in results] - --class ApiTag(object): -+class ApiTag(object): # pylint: disable=C0111 - def __init__(self, client): - self._client = client - diff --unified --recursive '--exclude=.pylint-ignores.patch' original/auth.py patched/auth.py ---- original/auth.py 2013-08-16 18:13:24.966482000 +0100 -+++ patched/auth.py 2013-08-16 18:13:51.766615537 +0100 +--- original/auth.py 2013-08-19 16:08:00.231047000 +0100 ++++ patched/auth.py 2013-08-19 16:09:30.263494209 +0100 @@ -4,7 +4,7 @@ from __future__ import unicode_literals import os @@ -95,8 +56,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/auth.py pa parser.readfp(buf) # Python2 diff --unified --recursive '--exclude=.pylint-ignores.patch' original/http.py patched/http.py ---- original/http.py 2013-08-16 17:54:30.688858000 +0100 -+++ patched/http.py 2013-08-16 18:14:14.106726301 +0100 +--- original/http.py 2013-08-19 16:09:27.459480000 +0100 ++++ patched/http.py 2013-08-19 16:09:46.311573793 +0100 @@ -7,18 +7,18 @@ import requests_oauthlib import logging @@ -106,7 +67,7 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/http.py pa except ImportError: from urlparse import urlparse, urlunparse # Python2 - from .objects import TroveboxObject + from trovebox.objects.trovebox_object import TroveboxObject -from .errors import * +from .errors import * # pylint: disable=W0401 from .auth import Auth @@ -130,8 +91,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/http.py pa token='', token_secret='', api_version=None): diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.py patched/__init__.py ---- original/__init__.py 2013-08-16 18:12:30.438212000 +0100 -+++ patched/__init__.py 2013-08-16 18:13:29.678506001 +0100 +--- original/__init__.py 2013-08-19 16:09:12.971408000 +0100 ++++ patched/__init__.py 2013-08-19 16:09:30.263494209 +0100 @@ -2,7 +2,7 @@ __init__.py : Trovebox package top level """ @@ -139,9 +100,9 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.p -from .errors import * +from .errors import * # pylint: disable=W0401 from ._version import __version__ - from . import api_photo - from . import api_tag -@@ -22,7 +22,7 @@ + from trovebox.api import api_photo + from trovebox.api import api_tag +@@ -23,7 +23,7 @@ This should be used to ensure that your application will continue to work even if the Trovebox API is updated to a new revision. """ @@ -151,8 +112,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.p token='', token_secret='', api_version=None): diff --unified --recursive '--exclude=.pylint-ignores.patch' original/main.py patched/main.py ---- original/main.py 2013-08-16 18:12:30.438212000 +0100 -+++ patched/main.py 2013-08-16 18:13:29.678506001 +0100 +--- original/main.py 2013-08-19 16:08:00.235047000 +0100 ++++ patched/main.py 2013-08-19 16:09:30.263494209 +0100 @@ -26,7 +26,7 @@ ################################################################# @@ -179,11 +140,13 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/main.py pa files[f].close() if options.verbose: -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects.py patched/objects.py ---- original/objects.py 2013-08-16 18:12:30.438212000 +0100 -+++ patched/objects.py 2013-08-16 18:13:29.682506021 +0100 -@@ -2,16 +2,16 @@ - objects.py : Basic Trovebox API Objects +diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/tag.py patched/objects/tag.py +--- original/objects/tag.py 2013-08-19 16:08:00.235047000 +0100 ++++ patched/objects/tag.py 2013-08-19 16:09:30.263494209 +0100 +@@ -1,8 +1,8 @@ +-""" ++""" # pylint: disable=R0801 + Representation of a Tag object """ try: - from urllib.parse import quote # Python3 @@ -191,8 +154,13 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects.py except ImportError: from urllib import quote # Python2 - from .errors import TroveboxError - +diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/trovebox_object.py patched/objects/trovebox_object.py +--- original/objects/trovebox_object.py 2013-08-19 16:08:00.235047000 +0100 ++++ patched/objects/trovebox_object.py 2013-08-19 16:09:30.263494209 +0100 +@@ -1,10 +1,10 @@ + """ + Base object supporting the storage of custom fields as attributes + """ -class TroveboxObject(object): +class TroveboxObject(object): # pylint: disable=R0903 """ Base object supporting the storage of custom fields as attributes """ @@ -202,37 +170,10 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects.py self.name = None self._trovebox = trovebox self._json_dict = json_dict -@@ -57,7 +57,7 @@ - return self._json_dict - - --class Photo(TroveboxObject): -+class Photo(TroveboxObject): # pylint: disable=C0111 - def delete(self, **kwds): - """ - Delete this photo. -@@ -147,7 +147,7 @@ - - self._replace_fields(new_dict) - --class Tag(TroveboxObject): -+class Tag(TroveboxObject): # pylint: disable=C0111 - def delete(self, **kwds): - """ - Delete this tag. -@@ -168,7 +168,7 @@ - self._replace_fields(new_dict) - - --class Album(TroveboxObject): -+class Album(TroveboxObject): # pylint: disable=C0111 - def __init__(self, trovebox, json_dict): - self.photos = None - self.cover = None diff --unified --recursive '--exclude=.pylint-ignores.patch' original/_version.py patched/_version.py ---- original/_version.py 2013-08-16 18:12:30.438212000 +0100 -+++ patched/_version.py 2013-08-16 18:13:29.682506021 +0100 +--- original/_version.py 2013-08-19 16:08:00.235047000 +0100 ++++ patched/_version.py 2013-08-19 16:09:30.263494209 +0100 @@ -1,2 +1,2 @@ - + # pylint: disable=C0111 - __version__ = "0.4" + __version__ = "0.5" diff --git a/trovebox/__init__.py b/trovebox/__init__.py index 18e1711..adb7ec1 100644 --- a/trovebox/__init__.py +++ b/trovebox/__init__.py @@ -4,9 +4,10 @@ __init__.py : Trovebox package top level from .http import Http from .errors import * from ._version import __version__ -from api import api_photo -from api import api_tag -from api import api_album +from trovebox.api import api_photo +from trovebox.api import api_tag +from trovebox.api import api_album +from trovebox.api import api_action LATEST_API_VERSION = 2 @@ -36,3 +37,4 @@ class Trovebox(Http): self.tag = api_tag.ApiTag(self) self.albums = api_album.ApiAlbums(self) self.album = api_album.ApiAlbum(self) + self.action = api_action.ApiAction(self) diff --git a/trovebox/http.py b/trovebox/http.py index 54222ef..ffe3b5f 100644 --- a/trovebox/http.py +++ b/trovebox/http.py @@ -11,7 +11,7 @@ try: except ImportError: from urlparse import urlparse, urlunparse # Python2 -from objects.trovebox_object import TroveboxObject +from trovebox.objects.trovebox_object import TroveboxObject from .errors import * from .auth import Auth From 55778dcd83da106a4873035c4f4ade71a81edd30 Mon Sep 17 00:00:00 2001 From: Pete Date: Mon, 19 Aug 2013 16:12:10 +0100 Subject: [PATCH 05/80] Add support for Action API Currently doesn't seem to work - server always returns an error --- tests/functional/test_actions.py | 17 ++++ tests/unit/test_actions.py | 142 +++++++++++++++++++++++++++++++ trovebox/api/api_action.py | 56 ++++++++++++ trovebox/objects/action.py | 47 ++++++++++ 4 files changed, 262 insertions(+) create mode 100644 tests/functional/test_actions.py create mode 100644 tests/unit/test_actions.py create mode 100644 trovebox/api/api_action.py create mode 100644 trovebox/objects/action.py diff --git a/tests/functional/test_actions.py b/tests/functional/test_actions.py new file mode 100644 index 0000000..28ed200 --- /dev/null +++ b/tests/functional/test_actions.py @@ -0,0 +1,17 @@ +try: + import unittest2 as unittest # Python2.6 +except ImportError: + import unittest + +from tests.functional import test_base + +class TestActionss(test_base.TestBase): + testcase_name = "action API" + + # TODO: Enable this test (and write more) once the Actions API is working. + # Currently always returns: + # "Could not find route /action/create.json from /action/create.json" + @unittest.expectedFailure + def test_create_delete(self): + """ Create an action on a photo, then delete it """ + action = self.client.action.create(target=self.photos[0]) diff --git a/tests/unit/test_actions.py b/tests/unit/test_actions.py new file mode 100644 index 0000000..6dd7ece --- /dev/null +++ b/tests/unit/test_actions.py @@ -0,0 +1,142 @@ +from __future__ import unicode_literals +import mock +try: + import unittest2 as unittest # Python2.6 +except ImportError: + import unittest + +import trovebox + +class TestActions(unittest.TestCase): + test_host = "test.example.com" + test_photos_dict = [{"id": "photo1"}, + {"id": "photo2"}] + test_actions_dict = [{"id": "1", + "target": test_photos_dict[0], + "target_type": "photo", + "totalRows": 2}, + {"id": "2", + "target": test_photos_dict[1], + "target_type": "photo", + "totalRows": 2}] + + def setUp(self): + self.client = trovebox.Trovebox(host=self.test_host) + self.test_photos = [trovebox.objects.photo.Photo(self.client, photo) + for photo in self.test_photos_dict] + self.test_actions = [trovebox.objects.action.Action(self.client, action) + for action in self.test_actions_dict] + + @staticmethod + def _return_value(result, message="", code=200): + return {"message": message, "code": code, "result": result} + +class TestActionCreate(TestActions): + @mock.patch.object(trovebox.Trovebox, 'post') + def test_action_create(self, mock_post): + """Check that an action can be created on a photo object""" + mock_post.return_value = self._return_value(self.test_actions_dict[0]) + result = self.client.action.create(target=self.test_photos[0], foo="bar") + mock_post.assert_called_with("/action/create.json", target=self.test_photos[0].id, + target_type="photo", + foo="bar") + self.assertEqual(result.id, "1") + self.assertEqual(result.target.id, "photo1") + self.assertEqual(result.target_type, "photo") + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_action_create_id(self, mock_post): + """Check that an action can be created using a photo id""" + mock_post.return_value = self._return_value(self.test_actions_dict[0]) + result = self.client.action.create(target=self.test_photos[0].id, + target_type="photo", foo="bar") + mock_post.assert_called_with("/action/create.json", target=self.test_photos[0].id, + target_type="photo", + foo="bar") + self.assertEqual(result.id, "1") + self.assertEqual(result.target.id, "photo1") + self.assertEqual(result.target_type, "photo") + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_action_create_invalid_type(self, mock_post): + """Check that an exception is raised if an action is created on a non photo object""" + with self.assertRaises(NotImplementedError): + self.client.action.create(target=object(), foo="bar") + +class TestActionDelete(TestActions): + @mock.patch.object(trovebox.Trovebox, 'post') + def test_action_delete(self, mock_post): + """Check that an action can be deleted""" + mock_post.return_value = self._return_value(True) + result = self.client.action.delete(self.test_actions[0]) + mock_post.assert_called_with("/action/1/delete.json") + self.assertEqual(result, True) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_action_delete_id(self, mock_post): + """Check that an action can be deleted using its ID""" + mock_post.return_value = self._return_value(True) + result = self.client.action.delete("1") + mock_post.assert_called_with("/action/1/delete.json") + self.assertEqual(result, True) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_action_delete_failure(self, mock_post): + """Check that an exception is raised if an action cannot be deleted""" + mock_post.return_value = self._return_value(False) + with self.assertRaises(trovebox.TroveboxError): + self.client.action.delete(self.test_actions[0]) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_action_object_delete(self, mock_post): + """Check that an action can be deleted using the action object directly""" + mock_post.return_value = self._return_value(True) + action = self.test_actions[0] + result = action.delete() + mock_post.assert_called_with("/action/1/delete.json") + self.assertEqual(result, True) + self.assertEqual(action.get_fields(), {}) + self.assertEqual(action.id, None) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_action_object_delete_failure(self, mock_post): + """ + Check that an exception is raised if an action cannot be deleted + when using the action object directly + """ + mock_post.return_value = self._return_value(False) + with self.assertRaises(trovebox.TroveboxError): + self.test_actions[0].delete() + +class TestActionView(TestActions): + @mock.patch.object(trovebox.Trovebox, 'get') + def test_action_view(self, mock_get): + """Check that an action can be viewed""" + mock_get.return_value = self._return_value(self.test_actions_dict[1]) + result = self.client.action.view(self.test_actions[0], name="Test") + mock_get.assert_called_with("/action/1/view.json", name="Test") + self.assertEqual(result.id, "2") + self.assertEqual(result.target.id, "photo2") + self.assertEqual(result.target_type, "photo") + + @mock.patch.object(trovebox.Trovebox, 'get') + def test_action_view_id(self, mock_get): + """Check that an action can be viewed using its ID""" + mock_get.return_value = self._return_value(self.test_actions_dict[1]) + result = self.client.action.view("1", name="Test") + mock_get.assert_called_with("/action/1/view.json", name="Test") + self.assertEqual(result.id, "2") + self.assertEqual(result.target.id, "photo2") + self.assertEqual(result.target_type, "photo") + + @mock.patch.object(trovebox.Trovebox, 'get') + def test_action_object_view(self, mock_get): + """Check that an action can be viewed using the action object directly""" + mock_get.return_value = self._return_value(self.test_actions_dict[1]) + action = self.test_actions[0] + action.view(name="Test") + mock_get.assert_called_with("/action/1/view.json", name="Test") + self.assertEqual(action.id, "2") + self.assertEqual(action.target.id, "photo2") + self.assertEqual(action.target_type, "photo") + diff --git a/trovebox/api/api_action.py b/trovebox/api/api_action.py new file mode 100644 index 0000000..1712a8a --- /dev/null +++ b/trovebox/api/api_action.py @@ -0,0 +1,56 @@ +""" +api_action.py : Trovebox Action API Classes +""" +from trovebox.objects.action import Action +from trovebox.objects.photo import Photo + +class ApiAction(object): + """ Definitions of /action/ API endpoints """ + def __init__(self, client): + self._client = client + + def create(self, target, target_type=None, **kwds): + """ + Create a new action and return it. + If the target_type parameter isn't specified, it is automatically + generated. + """ + if target_type is None: + # Determine the target type + if isinstance(target, Photo): + target_type = "photo" + else: + raise NotImplementedError("Actions can only be assigned to " + "Photos when target_type isn't " + "specified") + # Extract the ID from the target + try: + target_id = target.id + except AttributeError: + # Assume the ID was passed in directly + target_id = target + + result = self._client.post("/action/create.json", + target=target_id, target_type=target_type, + **kwds)["result"] + return Action(self._client, result) + + def delete(self, action, **kwds): + """ + Delete an action. + Returns True if successful. + Raises a TroveboxError if not. + """ + if not isinstance(action, Action): + action = Action(self._client, {"id": action}) + return action.delete(**kwds) + + def view(self, action, **kwds): + """ + View an action's contents. + Returns the requested action object. + """ + if not isinstance(action, Action): + action = Action(self._client, {"id": action}) + action.view(**kwds) + return action diff --git a/trovebox/objects/action.py b/trovebox/objects/action.py new file mode 100644 index 0000000..c15af37 --- /dev/null +++ b/trovebox/objects/action.py @@ -0,0 +1,47 @@ +""" +Representation of an Action object +""" +from trovebox.errors import TroveboxError +from .trovebox_object import TroveboxObject +from .photo import Photo + +class Action(TroveboxObject): + """ Representation of an Action object """ + def __init__(self, trovebox, json_dict): + self.target = None + self.target_type = None + TroveboxObject.__init__(self, trovebox, json_dict) + self._update_fields_with_objects() + + def _update_fields_with_objects(self): + """ Convert dict fields into objects, where appropriate """ + # Update the photo target with photo objects + if self.target is not None: + if self.target_type == "photo": + self.target = Photo(self._trovebox, self.target) + else: + raise NotImplementedError("Actions can only be assigned to " + "Photos") + + def delete(self, **kwds): + """ + Delete this action. + Returns True if successful. + Raises a TroveboxError if not. + """ + result = self._trovebox.post("/action/%s/delete.json" % + self.id, **kwds)["result"] + if not result: + raise TroveboxError("Delete response returned False") + self._delete_fields() + return result + + def view(self, **kwds): + """ + Requests the full contents of the action. + Updates the action's fields with the response. + """ + result = self._trovebox.get("/action/%s/view.json" % + self.id, **kwds)["result"] + self._replace_fields(result) + self._update_fields_with_objects() From 707eb270eec614a9f2f531c07917f0c97de066b6 Mon Sep 17 00:00:00 2001 From: Pete Date: Mon, 19 Aug 2013 18:12:29 +0100 Subject: [PATCH 06/80] Add activity endpoint support --- tests/functional/test_actions.py | 2 +- tests/functional/test_activities.py | 50 ++++++++++++++ tests/functional/test_base.py | 7 +- tests/functional/test_photos.py | 2 +- tests/unit/test_activities.py | 103 ++++++++++++++++++++++++++++ trovebox/.pylint-ignores.patch | 59 +++++++++++----- trovebox/__init__.py | 3 + trovebox/api/api_activity.py | 36 ++++++++++ trovebox/objects/activity.py | 40 +++++++++++ 9 files changed, 278 insertions(+), 24 deletions(-) create mode 100644 tests/functional/test_activities.py create mode 100644 tests/unit/test_activities.py create mode 100644 trovebox/api/api_activity.py create mode 100644 trovebox/objects/activity.py diff --git a/tests/functional/test_actions.py b/tests/functional/test_actions.py index 28ed200..fce9b89 100644 --- a/tests/functional/test_actions.py +++ b/tests/functional/test_actions.py @@ -5,7 +5,7 @@ except ImportError: from tests.functional import test_base -class TestActionss(test_base.TestBase): +class TestActions(test_base.TestBase): testcase_name = "action API" # TODO: Enable this test (and write more) once the Actions API is working. diff --git a/tests/functional/test_activities.py b/tests/functional/test_activities.py new file mode 100644 index 0000000..3c3f68f --- /dev/null +++ b/tests/functional/test_activities.py @@ -0,0 +1,50 @@ +try: + import unittest2 as unittest # Python2.6 +except ImportError: + import unittest + +from tests.functional import test_base + +class TestActivities(test_base.TestBase): + testcase_name = "activity API" + + def test_list(self): + """ + Upload three photos, and check that three corresponding activities + are created. + """ + self._delete_all() + self._create_test_photos(tag=False) + photos = self.client.photos.list() + + # Check that each activity is for a valid test photo + activities = self.client.activities.list() + self.assertEqual(len(activities), len(self.photos)) + for activity in activities: + self.assertIn(activity.data.id, [photo.id for photo in photos]) + + # The purge endpoint currently reports a 500: Internal Server Error + @unittest.expectedFailure + def test_purge(self): + """ Test that the purge endpoint deletes all activities """ + activities = self.client.activities.list() + self.assertNotEqual(activities, []) + self.client.activities.purge() + self.assertEqual(activities, []) + + def test_view(self): + """ Test that the view endpoint is working correctly """ + activity = self.client.activities.list()[0] + fields = activity.get_fields().copy() + + # Check that the view method returns the same data as the list + activity.view() + self.assertEqual(fields, activity.get_fields()) + + # Check using the Trovebox class + activity = self.client.activity.view(activity) + self.assertEqual(fields, activity.get_fields()) + + # Check passing the activity ID to the Trovebox class + activity = self.client.activity.view(activity.id) + self.assertEqual(fields, activity.get_fields()) diff --git a/tests/functional/test_base.py b/tests/functional/test_base.py index 9c7056c..8a67d0b 100644 --- a/tests/functional/test_base.py +++ b/tests/functional/test_base.py @@ -124,7 +124,7 @@ class TestBase(unittest.TestCase): logging.info("Finished %s\n", self.id()) @classmethod - def _create_test_photos(cls): + def _create_test_photos(cls, tag=True): """ Upload three test photos """ album = cls.client.album.create(cls.TEST_ALBUM) photos = [ @@ -139,8 +139,9 @@ class TestBase(unittest.TestCase): albums=album.id), ] # Add the test tag, removing any autogenerated tags - for photo in photos: - photo.update(tags=cls.TEST_TAG) + if tag: + for photo in photos: + photo.update(tags=cls.TEST_TAG) @classmethod def _delete_all(cls): diff --git a/tests/functional/test_photos.py b/tests/functional/test_photos.py index f153b49..6a7577e 100644 --- a/tests/functional/test_photos.py +++ b/tests/functional/test_photos.py @@ -18,7 +18,7 @@ class TestPhotos(test_base.TestBase): # Check that they're gone self.assertEqual(self.client.photos.list(), []) - # Re-upload the photos, one of them using Bas64 encoding + # Re-upload the photos, one of them using Base64 encoding ret_val = self.client.photo.upload("tests/data/test_photo1.jpg", title=self.TEST_TITLE) self.client.photo.upload("tests/data/test_photo2.jpg", diff --git a/tests/unit/test_activities.py b/tests/unit/test_activities.py new file mode 100644 index 0000000..55a89ee --- /dev/null +++ b/tests/unit/test_activities.py @@ -0,0 +1,103 @@ +from __future__ import unicode_literals +import json +import mock +try: + import unittest2 as unittest # Python2.6 +except ImportError: + import unittest + +import trovebox + +class TestActivities(unittest.TestCase): + test_host = "test.example.com" + test_photos_dict = [{"id": "photo1"}, + {"id": "photo2"}] + test_activities_dict = [{"id": "1", + "data": test_photos_dict[0], + "type": "photo_upload"}, + {"id": "2", + "data": test_photos_dict[1], + "type": "photo_update"}] + + def setUp(self): + self.client = trovebox.Trovebox(host=self.test_host) + self.test_photos = [trovebox.objects.photo.Photo(self.client, photo) + for photo in self.test_photos_dict] + self.test_activities = [trovebox.objects.activity.Activity(self.client, activity) + for activity in self.test_activities_dict] + + @staticmethod + def _return_value(result, message="", code=200): + return {"message": message, "code": code, "result": result} + + @staticmethod + def _view_wrapper(result): + """ The view method returns data enclosed in a dict and JSON encoded """ + result["data"] = json.dumps(result["data"]) + return {"0": result} + +class TestActivitiesList(TestActivities): + @mock.patch.object(trovebox.Trovebox, 'get') + def test_activities_list(self, mock_get): + """Check that the activity list is returned correctly""" + mock_get.return_value = self._return_value(self.test_activities_dict) + + result = self.client.activities.list() + mock_get.assert_called_with("/activities/list.json") + self.assertEqual(len(result), 2) + self.assertEqual(result[0].id, "1") + self.assertEqual(result[0].type, "photo_upload") + self.assertEqual(result[0].data.id, "photo1") + self.assertEqual(result[1].id, "2") + self.assertEqual(result[1].type, "photo_update") + self.assertEqual(result[1].data.id, "photo2") + +class TestActivitiesPurge(TestActivities): + @mock.patch.object(trovebox.Trovebox, 'post') + def test_activity_purge(self, mock_get): + """Test activity purging """ + mock_get.return_value = self._return_value(True) + + result = self.client.activities.purge(foo="bar") + mock_get.assert_called_with("/activities/purge.json", foo="bar") + self.assertEqual(result, True) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_activity_purge_failure(self, mock_post): + """Test activity purging """ + mock_post.return_value = self._return_value(False) + with self.assertRaises(trovebox.TroveboxError): + result = self.client.activities.purge(foo="bar") + +class TestActivityView(TestActivities): + @mock.patch.object(trovebox.Trovebox, 'get') + def test_activity_view(self, mock_get): + """Check that a activity can be viewed""" + mock_get.return_value = self._return_value(self._view_wrapper( + self.test_activities_dict[1])) + result = self.client.activity.view(self.test_activities[0], + foo="bar") + mock_get.assert_called_with("/activity/1/view.json", foo="bar") + self.assertEqual(result.get_fields(), self.test_activities_dict[1]) + + @mock.patch.object(trovebox.Trovebox, 'get') + def test_activity_view_id(self, mock_get): + """Check that a activity can be viewed using its ID""" + mock_get.return_value = self._return_value(self._view_wrapper( + self.test_activities_dict[1])) + result = self.client.activity.view("1", foo="bar") + mock_get.assert_called_with("/activity/1/view.json", foo="bar") + self.assertEqual(result.get_fields(), self.test_activities_dict[1]) + + @mock.patch.object(trovebox.Trovebox, 'get') + def test_activity_object_view(self, mock_get): + """ + Check that a activity can be viewed + when using the activity object directly + """ + mock_get.return_value = self._return_value(self._view_wrapper( + self.test_activities_dict[1])) + activity = self.test_activities[0] + activity.view(foo="bar") + mock_get.assert_called_with("/activity/1/view.json", foo="bar") + self.assertEqual(activity.get_fields(), self.test_activities_dict[1]) diff --git a/trovebox/.pylint-ignores.patch b/trovebox/.pylint-ignores.patch index 3aa911d..a4d8f5d 100644 --- a/trovebox/.pylint-ignores.patch +++ b/trovebox/.pylint-ignores.patch @@ -1,6 +1,18 @@ +diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_activity.py patched/api/api_activity.py +--- original/api/api_activity.py 2013-08-19 17:59:15.592149000 +0100 ++++ patched/api/api_activity.py 2013-08-19 18:08:39.950947589 +0100 +@@ -22,7 +22,7 @@ + raise TroveboxError("Purge response returned False") + return True + +-class ApiActivity(object): ++class ApiActivity(object): # pylint: disable=R0903 + """ Definitions of /activity/ API endpoints """ + def __init__(self, client): + self._client = client diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_album.py patched/api/api_album.py ---- original/api/api_album.py 2013-08-19 16:08:00.231047000 +0100 -+++ patched/api/api_album.py 2013-08-19 16:09:30.263494209 +0100 +--- original/api/api_album.py 2013-08-19 16:09:53.539609000 +0100 ++++ patched/api/api_album.py 2013-08-19 18:08:20.118849270 +0100 @@ -3,7 +3,7 @@ """ from trovebox.objects.album import Album @@ -11,8 +23,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_al def __init__(self, client): self._client = client diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_tag.py patched/api/api_tag.py ---- original/api/api_tag.py 2013-08-19 16:08:00.231047000 +0100 -+++ patched/api/api_tag.py 2013-08-19 16:09:30.263494209 +0100 +--- original/api/api_tag.py 2013-08-19 16:09:53.539609000 +0100 ++++ patched/api/api_tag.py 2013-08-19 18:08:20.118849270 +0100 @@ -3,7 +3,7 @@ """ from trovebox.objects.tag import Tag @@ -23,8 +35,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_ta def __init__(self, client): self._client = client diff --unified --recursive '--exclude=.pylint-ignores.patch' original/auth.py patched/auth.py ---- original/auth.py 2013-08-19 16:08:00.231047000 +0100 -+++ patched/auth.py 2013-08-19 16:09:30.263494209 +0100 +--- original/auth.py 2013-08-19 16:09:53.543609000 +0100 ++++ patched/auth.py 2013-08-19 18:08:20.118849270 +0100 @@ -4,7 +4,7 @@ from __future__ import unicode_literals import os @@ -56,8 +68,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/auth.py pa parser.readfp(buf) # Python2 diff --unified --recursive '--exclude=.pylint-ignores.patch' original/http.py patched/http.py ---- original/http.py 2013-08-19 16:09:27.459480000 +0100 -+++ patched/http.py 2013-08-19 16:09:46.311573793 +0100 +--- original/http.py 2013-08-19 16:09:53.543609000 +0100 ++++ patched/http.py 2013-08-19 18:08:20.118849270 +0100 @@ -7,18 +7,18 @@ import requests_oauthlib import logging @@ -91,8 +103,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/http.py pa token='', token_secret='', api_version=None): diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.py patched/__init__.py ---- original/__init__.py 2013-08-19 16:09:12.971408000 +0100 -+++ patched/__init__.py 2013-08-19 16:09:30.263494209 +0100 +--- original/__init__.py 2013-08-19 17:02:22.951226000 +0100 ++++ patched/__init__.py 2013-08-19 18:08:36.194928993 +0100 @@ -2,7 +2,7 @@ __init__.py : Trovebox package top level """ @@ -102,7 +114,16 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.p from ._version import __version__ from trovebox.api import api_photo from trovebox.api import api_tag -@@ -23,7 +23,7 @@ +@@ -12,7 +12,7 @@ + + LATEST_API_VERSION = 2 + +-class Trovebox(Http): ++class Trovebox(Http): # pylint: disable=R0902 + """ + Client library for Trovebox + If no parameters are specified, config is loaded from the default +@@ -24,7 +24,7 @@ This should be used to ensure that your application will continue to work even if the Trovebox API is updated to a new revision. """ @@ -112,8 +133,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.p token='', token_secret='', api_version=None): diff --unified --recursive '--exclude=.pylint-ignores.patch' original/main.py patched/main.py ---- original/main.py 2013-08-19 16:08:00.235047000 +0100 -+++ patched/main.py 2013-08-19 16:09:30.263494209 +0100 +--- original/main.py 2013-08-19 16:09:53.543609000 +0100 ++++ patched/main.py 2013-08-19 18:08:20.118849270 +0100 @@ -26,7 +26,7 @@ ################################################################# @@ -141,8 +162,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/main.py pa if options.verbose: diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/tag.py patched/objects/tag.py ---- original/objects/tag.py 2013-08-19 16:08:00.235047000 +0100 -+++ patched/objects/tag.py 2013-08-19 16:09:30.263494209 +0100 +--- original/objects/tag.py 2013-08-19 16:09:53.543609000 +0100 ++++ patched/objects/tag.py 2013-08-19 18:08:20.118849270 +0100 @@ -1,8 +1,8 @@ -""" +""" # pylint: disable=R0801 @@ -155,8 +176,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/ta from urllib import quote # Python2 diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/trovebox_object.py patched/objects/trovebox_object.py ---- original/objects/trovebox_object.py 2013-08-19 16:08:00.235047000 +0100 -+++ patched/objects/trovebox_object.py 2013-08-19 16:09:30.263494209 +0100 +--- original/objects/trovebox_object.py 2013-08-19 16:09:53.543609000 +0100 ++++ patched/objects/trovebox_object.py 2013-08-19 18:08:20.118849270 +0100 @@ -1,10 +1,10 @@ """ Base object supporting the storage of custom fields as attributes @@ -171,8 +192,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/tr self._trovebox = trovebox self._json_dict = json_dict diff --unified --recursive '--exclude=.pylint-ignores.patch' original/_version.py patched/_version.py ---- original/_version.py 2013-08-19 16:08:00.235047000 +0100 -+++ patched/_version.py 2013-08-19 16:09:30.263494209 +0100 +--- original/_version.py 2013-08-19 16:09:53.543609000 +0100 ++++ patched/_version.py 2013-08-19 18:08:20.118849270 +0100 @@ -1,2 +1,2 @@ - + # pylint: disable=C0111 diff --git a/trovebox/__init__.py b/trovebox/__init__.py index adb7ec1..9189d35 100644 --- a/trovebox/__init__.py +++ b/trovebox/__init__.py @@ -8,6 +8,7 @@ from trovebox.api import api_photo from trovebox.api import api_tag from trovebox.api import api_album from trovebox.api import api_action +from trovebox.api import api_activity LATEST_API_VERSION = 2 @@ -38,3 +39,5 @@ class Trovebox(Http): self.albums = api_album.ApiAlbums(self) self.album = api_album.ApiAlbum(self) self.action = api_action.ApiAction(self) + self.activities = api_activity.ApiActivities(self) + self.activity = api_activity.ApiActivity(self) diff --git a/trovebox/api/api_activity.py b/trovebox/api/api_activity.py new file mode 100644 index 0000000..d3f4c42 --- /dev/null +++ b/trovebox/api/api_activity.py @@ -0,0 +1,36 @@ +""" +api_activity.py : Trovebox Activity API Classes +""" +from trovebox.errors import TroveboxError +from trovebox.objects.activity import Activity + +class ApiActivities(object): + """ Definitions of /activities/ API endpoints """ + def __init__(self, client): + self._client = client + + def list(self, **kwds): + """ Returns a list of Activity objects """ + activities = self._client.get("/activities/list.json", **kwds)["result"] + return [Activity(self._client, activity) for activity in activities] + + def purge(self, **kwds): + """ Purge all activities """ + if not self._client.post("/activities/purge.json", **kwds)["result"]: + raise TroveboxError("Purge response returned False") + return True + +class ApiActivity(object): + """ Definitions of /activity/ API endpoints """ + def __init__(self, client): + self._client = client + + def view(self, activity, **kwds): + """ + View an activity's contents. + Returns the requested activity object. + """ + if not isinstance(activity, Activity): + activity = Activity(self._client, {"id": activity}) + activity.view(**kwds) + return activity diff --git a/trovebox/objects/activity.py b/trovebox/objects/activity.py new file mode 100644 index 0000000..fe57013 --- /dev/null +++ b/trovebox/objects/activity.py @@ -0,0 +1,40 @@ +""" +Representation of an Activity object +""" +import json + +from .trovebox_object import TroveboxObject +from .photo import Photo + +class Activity(TroveboxObject): + """ Representation of an Activity object """ + def __init__(self, trovebox, json_dict): + self.data = None + self.type = None + TroveboxObject.__init__(self, trovebox, json_dict) + self._update_fields_with_objects() + + def _update_fields_with_objects(self): + """ Convert dict fields into objects, where appropriate """ + # Update the data with photo objects + if self.type is not None: + if self.type.startswith("photo"): + self.data = Photo(self._trovebox, self.data) + else: + raise NotImplementedError("Unrecognised activity type: %s" + % self.type) + + def view(self, **kwds): + """ + Requests the full contents of the activity. + Updates the activity's fields with the response. + """ + result = self._trovebox.get("/activity/%s/view.json" % + self.id, **kwds)["result"] + + # TBD: Why is the result enclosed/encoded like this? + result = result["0"] + result["data"] = json.loads(result["data"]) + + self._replace_fields(result) + self._update_fields_with_objects() From 5a0065a8733065cd0fe4b052d04fa7a248849d64 Mon Sep 17 00:00:00 2001 From: Pete Date: Mon, 26 Aug 2013 11:20:38 -0700 Subject: [PATCH 07/80] Fix typo --- tests/unit/test_http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_http.py b/tests/unit/test_http.py index bf34f3c..79f881b 100644 --- a/tests/unit/test_http.py +++ b/tests/unit/test_http.py @@ -172,7 +172,7 @@ class TestHttp(unittest.TestCase): """Check that the parameter processing function is working""" self._register_uri(httpretty.GET) photo = trovebox.objects.photo.Photo(None, {"id": "photo_id"}) - album = trovebox.objects.photo.Album(None, {"id": "album_id"}) + album = trovebox.objects.album.Album(None, {"id": "album_id"}) tag = trovebox.objects.tag.Tag(None, {"id": "tag_id"}) self.client.get(self.test_endpoint, photo=photo, album=album, tag=tag, From ade2fac3cb6d218601b1571852c9eb1eb89878f2 Mon Sep 17 00:00:00 2001 From: Pete Date: Mon, 26 Aug 2013 11:50:07 -0700 Subject: [PATCH 08/80] Add testcase for partially populated config files --- tests/unit/test_auth.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_auth.py b/tests/unit/test_auth.py index bf70160..a3623c8 100644 --- a/tests/unit/test_auth.py +++ b/tests/unit/test_auth.py @@ -31,7 +31,8 @@ class TestAuth(unittest.TestCase): def create_config(config_file, host): """Create a dummy config file""" with open(os.path.join(CONFIG_PATH, config_file), "w") as conf: - conf.write("host = %s\n" % host) + if host is not None: + conf.write("host = %s\n" % host) conf.write("# Comment\n\n") conf.write("consumerKey = \"%s_consumer_key\"\n" % config_file) conf.write("\"consumerSecret\"= %s_consumer_secret\n" % config_file) @@ -97,4 +98,13 @@ class TestAuth(unittest.TestCase): with self.assertRaises(ValueError): Trovebox(config_file="custom", host="host_override") - + def test_partial_config_file(self): + """ Test that an incomplete config file causes default values to be set """ + self.create_config("incomplete", host=None) # Don't write the host line + client = Trovebox(config_file="incomplete") + auth = client.auth + self.assertEqual(auth.host, "localhost") + self.assertEqual(auth.consumer_key, "incomplete_consumer_key") + self.assertEqual(auth.consumer_secret, "incomplete_consumer_secret") + self.assertEqual(auth.token, "incomplete_token") + self.assertEqual(auth.token_secret, "incomplete_token_secret") From ae4295ee4e810620d90b044b6a2132a3b6708d98 Mon Sep 17 00:00:00 2001 From: Pete Date: Mon, 26 Aug 2013 11:50:24 -0700 Subject: [PATCH 09/80] Add testcase for tag create endpoint --- tests/unit/test_tags.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/unit/test_tags.py b/tests/unit/test_tags.py index bc883fb..e0c469f 100644 --- a/tests/unit/test_tags.py +++ b/tests/unit/test_tags.py @@ -35,6 +35,15 @@ class TestTagsList(TestTags): self.assertEqual(result[1].id, "tag2") self.assertEqual(result[1].count, 5) +class TestTagCreate(TestTags): + @mock.patch.object(trovebox.Trovebox, 'post') + def test_tag_create(self, mock_post): + """Check that a tag can be created""" + mock_post.return_value = self._return_value(True) + result = self.client.tag.create("test") + mock_post.assert_called_with("/tag/create.json", tag="test") + self.assertEqual(result, True) + class TestTagDelete(TestTags): @mock.patch.object(trovebox.Trovebox, 'post') def test_tag_delete(self, mock_post): From 138f47add30680626e383ebc0c0d9920534cbbad Mon Sep 17 00:00:00 2001 From: Pete Date: Mon, 26 Aug 2013 11:57:02 -0700 Subject: [PATCH 10/80] Use result_to_list for all list endpoints, to ensure empty lists are handled correctly --- trovebox/api/api_activity.py | 2 ++ trovebox/api/api_album.py | 6 ++++-- trovebox/api/api_photo.py | 2 +- trovebox/api/api_tag.py | 6 ++++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/trovebox/api/api_activity.py b/trovebox/api/api_activity.py index d3f4c42..0994cd0 100644 --- a/trovebox/api/api_activity.py +++ b/trovebox/api/api_activity.py @@ -1,6 +1,7 @@ """ api_activity.py : Trovebox Activity API Classes """ +from trovebox import http from trovebox.errors import TroveboxError from trovebox.objects.activity import Activity @@ -12,6 +13,7 @@ class ApiActivities(object): def list(self, **kwds): """ Returns a list of Activity objects """ activities = self._client.get("/activities/list.json", **kwds)["result"] + activities = http.result_to_list(activities) return [Activity(self._client, activity) for activity in activities] def purge(self, **kwds): diff --git a/trovebox/api/api_album.py b/trovebox/api/api_album.py index 2767c3f..4326816 100644 --- a/trovebox/api/api_album.py +++ b/trovebox/api/api_album.py @@ -2,6 +2,7 @@ api_album.py : Trovebox Album API Classes """ from trovebox.objects.album import Album +from trovebox import http class ApiAlbums(object): """ Definitions of /albums/ API endpoints """ @@ -10,8 +11,9 @@ class ApiAlbums(object): def list(self, **kwds): """ Return a list of Album objects """ - results = self._client.get("/albums/list.json", **kwds)["result"] - return [Album(self._client, album) for album in results] + albums = self._client.get("/albums/list.json", **kwds)["result"] + albums = http.result_to_list(albums) + return [Album(self._client, album) for album in albums] class ApiAlbum(object): """ Definitions of /album/ API endpoints """ diff --git a/trovebox/api/api_photo.py b/trovebox/api/api_photo.py index ee835e4..b5f8512 100644 --- a/trovebox/api/api_photo.py +++ b/trovebox/api/api_photo.py @@ -3,8 +3,8 @@ api_photo.py : Trovebox Photo API Classes """ import base64 -from trovebox.errors import TroveboxError from trovebox import http +from trovebox.errors import TroveboxError from trovebox.objects.photo import Photo def extract_ids(photos): diff --git a/trovebox/api/api_tag.py b/trovebox/api/api_tag.py index e46afda..acd6fca 100644 --- a/trovebox/api/api_tag.py +++ b/trovebox/api/api_tag.py @@ -1,6 +1,7 @@ """ api_tag.py : Trovebox Tag API Classes """ +from trovebox import http from trovebox.objects.tag import Tag class ApiTags(object): @@ -10,8 +11,9 @@ class ApiTags(object): def list(self, **kwds): """ Returns a list of Tag objects """ - results = self._client.get("/tags/list.json", **kwds)["result"] - return [Tag(self._client, tag) for tag in results] + tags = self._client.get("/tags/list.json", **kwds)["result"] + tags = http.result_to_list(tags) + return [Tag(self._client, tag) for tag in tags] class ApiTag(object): """ Definitions of /tag/ API endpoints """ From c18e590cc21042144573be61b73ae2e4ab7d2e82 Mon Sep 17 00:00:00 2001 From: Pete Date: Mon, 26 Aug 2013 12:09:33 -0700 Subject: [PATCH 11/80] Test that all types of empty lists are retuned as [] --- tests/unit/test_activities.py | 16 ++++++++++++++++ tests/unit/test_albums.py | 16 ++++++++++++++++ tests/unit/test_photos.py | 16 ++++++++++++++++ tests/unit/test_tags.py | 18 +++++++++++++++++- 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_activities.py b/tests/unit/test_activities.py index 55a89ee..976197b 100644 --- a/tests/unit/test_activities.py +++ b/tests/unit/test_activities.py @@ -52,6 +52,22 @@ class TestActivitiesList(TestActivities): self.assertEqual(result[1].type, "photo_update") self.assertEqual(result[1].data.id, "photo2") + @mock.patch.object(trovebox.Trovebox, 'get') + def test_empty_result(self, mock_get): + """Check that an empty result is transformed into an empty list """ + mock_get.return_value = self._return_value("") + result = self.client.activities.list() + mock_get.assert_called_with("/activities/list.json") + self.assertEqual(result, []) + + @mock.patch.object(trovebox.Trovebox, 'get') + def test_zero_rows(self, mock_get): + """Check that totalRows=0 is transformed into an empty list """ + mock_get.return_value = self._return_value([{"totalRows": 0}]) + result = self.client.activities.list() + mock_get.assert_called_with("/activities/list.json") + self.assertEqual(result, []) + class TestActivitiesPurge(TestActivities): @mock.patch.object(trovebox.Trovebox, 'post') def test_activity_purge(self, mock_get): diff --git a/tests/unit/test_albums.py b/tests/unit/test_albums.py index db91e31..513500c 100644 --- a/tests/unit/test_albums.py +++ b/tests/unit/test_albums.py @@ -39,6 +39,22 @@ class TestAlbumsList(TestAlbums): self.assertEqual(result[1].id, "2") self.assertEqual(result[1].name, "Album 2") + @mock.patch.object(trovebox.Trovebox, 'get') + def test_empty_result(self, mock_get): + """Check that an empty result is transformed into an empty list """ + mock_get.return_value = self._return_value("") + result = self.client.albums.list() + mock_get.assert_called_with("/albums/list.json") + self.assertEqual(result, []) + + @mock.patch.object(trovebox.Trovebox, 'get') + def test_zero_rows(self, mock_get): + """Check that totalRows=0 is transformed into an empty list """ + mock_get.return_value = self._return_value([{"totalRows": 0}]) + result = self.client.albums.list() + mock_get.assert_called_with("/albums/list.json") + self.assertEqual(result, []) + @mock.patch.object(trovebox.Trovebox, 'get') def test_albums_list_returns_cover_photos(self, mock_get): """Check that the album list returns cover photo objects""" diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index def92f0..c1ed191 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -39,6 +39,22 @@ class TestPhotosList(TestPhotos): self.assertEqual(result[1].id, "2b") self.assertEqual(result[1].tags, ["tag3", "tag4"]) + @mock.patch.object(trovebox.Trovebox, 'get') + def test_empty_result(self, mock_get): + """Check that an empty result is transformed into an empty list """ + mock_get.return_value = self._return_value("") + result = self.client.photos.list() + mock_get.assert_called_with("/photos/list.json") + self.assertEqual(result, []) + + @mock.patch.object(trovebox.Trovebox, 'get') + def test_zero_rows(self, mock_get): + """Check that totalRows=0 is transformed into an empty list """ + mock_get.return_value = self._return_value([{"totalRows": 0}]) + result = self.client.photos.list() + mock_get.assert_called_with("/photos/list.json") + self.assertEqual(result, []) + class TestPhotosUpdate(TestPhotos): @mock.patch.object(trovebox.Trovebox, 'post') def test_photos_update(self, mock_post): diff --git a/tests/unit/test_tags.py b/tests/unit/test_tags.py index e0c469f..fd5b88a 100644 --- a/tests/unit/test_tags.py +++ b/tests/unit/test_tags.py @@ -25,7 +25,7 @@ class TestTags(unittest.TestCase): class TestTagsList(TestTags): @mock.patch.object(trovebox.Trovebox, 'get') def test_tags_list(self, mock_get): - """Check that the the tag list is returned correctly""" + """Check that the tag list is returned correctly""" mock_get.return_value = self._return_value(self.test_tags_dict) result = self.client.tags.list() mock_get.assert_called_with("/tags/list.json") @@ -35,6 +35,22 @@ class TestTagsList(TestTags): self.assertEqual(result[1].id, "tag2") self.assertEqual(result[1].count, 5) + @mock.patch.object(trovebox.Trovebox, 'get') + def test_empty_result(self, mock_get): + """Check that an empty result is transformed into an empty list """ + mock_get.return_value = self._return_value("") + result = self.client.tags.list() + mock_get.assert_called_with("/tags/list.json") + self.assertEqual(result, []) + + @mock.patch.object(trovebox.Trovebox, 'get') + def test_zero_rows(self, mock_get): + """Check that totalRows=0 is transformed into an empty list """ + mock_get.return_value = self._return_value([{"totalRows": 0}]) + result = self.client.tags.list() + mock_get.assert_called_with("/tags/list.json") + self.assertEqual(result, []) + class TestTagCreate(TestTags): @mock.patch.object(trovebox.Trovebox, 'post') def test_tag_create(self, mock_post): From 2237d438548129c800ba1e75f3de113642f58f12 Mon Sep 17 00:00:00 2001 From: Pete Date: Mon, 26 Aug 2013 12:27:54 -0700 Subject: [PATCH 12/80] Add tests for cli help and parameter verbosity --- tests/unit/test_cli.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 596649d..4eb7b2a 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -108,9 +108,10 @@ class TestCli(unittest.TestCase): @mock.patch('sys.stdout', new_callable=io.StringIO) def test_verbose(self, mock_stdout, _): """Check that the verbose option is working""" - main(["-v"]) + main(["-v", "-F foo=bar"]) self.assertIn("Method: GET", mock_stdout.getvalue()) self.assertIn("Endpoint: /photos/list.json", mock_stdout.getvalue()) + self.assertIn("foo=bar", mock_stdout.getvalue()) @mock.patch.object(trovebox.main.trovebox, "Trovebox") @mock.patch('sys.stdout', new_callable=io.StringIO) @@ -127,3 +128,8 @@ class TestCli(unittest.TestCase): main(["--version"]) self.assertEqual(mock_stdout.getvalue(), trovebox.__version__ + "\n") + @mock.patch('sys.stdout', new_callable=io.StringIO) + def test_help(self, mock_stdout): + """Check that the help string is correctly printed""" + main(["--help"]) + self.assertIn("show this help message", mock_stdout.getvalue()) From a97851b82ab2e0973033168967a44335d534d8e2 Mon Sep 17 00:00:00 2001 From: Pete Date: Mon, 26 Aug 2013 12:46:20 -0700 Subject: [PATCH 13/80] Add tests for invalid return types --- tests/unit/test_actions.py | 8 ++++++++ tests/unit/test_activities.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/tests/unit/test_actions.py b/tests/unit/test_actions.py index 6dd7ece..3028dbf 100644 --- a/tests/unit/test_actions.py +++ b/tests/unit/test_actions.py @@ -63,6 +63,14 @@ class TestActionCreate(TestActions): with self.assertRaises(NotImplementedError): self.client.action.create(target=object(), foo="bar") + @mock.patch.object(trovebox.Trovebox, 'post') + def test_action_create_invalid_return_type(self, mock_post): + """Check that an exception is raised if an non photo object is returned""" + mock_post.return_value = self._return_value({"target": "test", + "target_type": "invalid"}) + with self.assertRaises(NotImplementedError): + self.client.action.create(target=self.test_photos[0], foo="bar") + class TestActionDelete(TestActions): @mock.patch.object(trovebox.Trovebox, 'post') def test_action_delete(self, mock_post): diff --git a/tests/unit/test_activities.py b/tests/unit/test_activities.py index 976197b..0ea190f 100644 --- a/tests/unit/test_activities.py +++ b/tests/unit/test_activities.py @@ -117,3 +117,11 @@ class TestActivityView(TestActivities): activity.view(foo="bar") mock_get.assert_called_with("/activity/1/view.json", foo="bar") self.assertEqual(activity.get_fields(), self.test_activities_dict[1]) + + @mock.patch.object(trovebox.Trovebox, 'get') + def test_activity_view_invalid_type(self, mock_get): + """Check that an invalid activity type raises an exception""" + mock_get.return_value = self._return_value(self._view_wrapper( + {"data": "", "type": "invalid"})) + with self.assertRaises(NotImplementedError): + self.client.activity.view(self.test_activities[0], foo="bar") From 7e4fb52d939b7d9f63bcfc6dac4e0db7849ad433 Mon Sep 17 00:00:00 2001 From: Pete Date: Mon, 26 Aug 2013 12:50:18 -0700 Subject: [PATCH 14/80] Remove unused album.photos attribute. --- trovebox/objects/album.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/trovebox/objects/album.py b/trovebox/objects/album.py index 853370c..62b898c 100644 --- a/trovebox/objects/album.py +++ b/trovebox/objects/album.py @@ -8,7 +8,6 @@ from .photo import Photo class Album(TroveboxObject): """ Representation of an Album object """ def __init__(self, trovebox, json_dict): - self.photos = None self.cover = None TroveboxObject.__init__(self, trovebox, json_dict) self._update_fields_with_objects() @@ -18,11 +17,6 @@ class Album(TroveboxObject): # Update the cover with a photo object if isinstance(self.cover, dict): self.cover = Photo(self._trovebox, self.cover) - # Update the photo list with photo objects - if isinstance(self.photos, list): - for i, photo in enumerate(self.photos): - if isinstance(photo, dict): - self.photos[i] = Photo(self._trovebox, photo) def delete(self, **kwds): """ @@ -55,7 +49,7 @@ class Album(TroveboxObject): self.id, **kwds)["result"] # APIv1 doesn't return the updated album (frontend issue #937) - if isinstance(result, bool): + if isinstance(result, bool): # pragma: no cover result = self._trovebox.get("/album/%s/view.json" % self.id)["result"] From f742df27b472e958facf93c9d1b8cdb30132836c Mon Sep 17 00:00:00 2001 From: Pete Date: Mon, 26 Aug 2013 12:57:47 -0700 Subject: [PATCH 15/80] Add coverage ignore pragmas --- trovebox/auth.py | 2 +- trovebox/http.py | 6 +++--- trovebox/main.py | 2 +- trovebox/objects/photo.py | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/trovebox/auth.py b/trovebox/auth.py index 378ee65..b3e29ed 100644 --- a/trovebox/auth.py +++ b/trovebox/auth.py @@ -9,7 +9,7 @@ except ImportError: from ConfigParser import SafeConfigParser as ConfigParser # Python2 try: import io # Python3 -except ImportError: +except ImportError: # pragma: no cover import StringIO as io # Python2 class Auth(object): diff --git a/trovebox/http.py b/trovebox/http.py index ffe3b5f..dd33917 100644 --- a/trovebox/http.py +++ b/trovebox/http.py @@ -17,7 +17,7 @@ from .auth import Auth if sys.version < '3': TEXT_TYPE = unicode -else: +else: # pragma: no cover TEXT_TYPE = str DUPLICATE_RESPONSE = {"code": 409, @@ -43,7 +43,7 @@ class Http(object): self.config = dict(self._CONFIG_DEFAULTS) - if api_version is not None: + if api_version is not None: # pragma: no cover print("Deprecation Warning: api_version should be set by " "calling the configure function") self.config["api_version"] = api_version @@ -246,7 +246,7 @@ def result_to_list(result): """ Handle the case where the result contains no items """ if not result: return [] - if result[0]["totalRows"] == 0: + if "totalRows" in result[0] and result[0]["totalRows"] == 0: return [] else: return result diff --git a/trovebox/main.py b/trovebox/main.py index b7b84e4..b9b9f4f 100644 --- a/trovebox/main.py +++ b/trovebox/main.py @@ -129,5 +129,5 @@ def extract_files(params): return updated_params, files -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover main() diff --git a/trovebox/objects/photo.py b/trovebox/objects/photo.py index a5b3f4e..ffb66af 100644 --- a/trovebox/objects/photo.py +++ b/trovebox/objects/photo.py @@ -62,7 +62,7 @@ class Photo(TroveboxObject): value = {} if "next" in result: # Workaround for APIv1 - if not isinstance(result["next"], list): + if not isinstance(result["next"], list): # pragma: no cover result["next"] = [result["next"]] value["next"] = [] @@ -71,7 +71,7 @@ class Photo(TroveboxObject): if "previous" in result: # Workaround for APIv1 - if not isinstance(result["previous"], list): + if not isinstance(result["previous"], list): # pragma: no cover result["previous"] = [result["previous"]] value["previous"] = [] @@ -89,7 +89,7 @@ class Photo(TroveboxObject): self.id, **kwds)["result"] # APIv1 doesn't return the transformed photo (frontend issue #955) - if isinstance(result, bool): + if isinstance(result, bool): # pragma: no cover result = self._trovebox.get("/photo/%s/view.json" % self.id)["result"] From 431e8fcc8aaff19d3ddcfc6bb7ae769005606554 Mon Sep 17 00:00:00 2001 From: Pete Date: Thu, 29 Aug 2013 14:23:09 -0700 Subject: [PATCH 16/80] Fix object repr functions --- tests/unit/test_photos.py | 28 ++++++++++++++++++++++++++++ trovebox/objects/trovebox_object.py | 6 +++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index c1ed191..05500d2 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -443,3 +443,31 @@ class TestPhotoTransform(TestPhotos): photo.transform(rotate="90") mock_post.assert_called_with("/photo/1a/transform.json", rotate="90") self.assertEqual(photo.get_fields(), self.test_photos_dict[1]) + +class TestPhotoObject(TestPhotos): + def test_photo_object_repr_without_id_or_name(self): + """ + Ensure the string representation on an object includes its class name + if the ID and Name attributes don't exist. + """ + photo = trovebox.objects.photo.Photo(self.client, {}) + self.assertEqual(repr(photo), "") + + def test_photo_object_repr_with_id(self): + """ Ensure the string representation on an object includes its id, if present """ + photo = trovebox.objects.photo.Photo(self.client, {"id": "Test ID"}) + self.assertEqual(repr(photo), "") + + def test_photo_object_repr_with_id_and_name(self): + """ Ensure the string representation on an object includes its name, if present """ + photo = trovebox.objects.photo.Photo(self.client, {"id": "Test ID", + "name": "Test Name"}) + self.assertEqual(repr(photo), "") + + def test_photo_object_illegal_attribute(self): + """ + Check that an exception is raised when creating an Photo object + with an illegal attribute + """ + with self.assertRaises(ValueError): + photo = trovebox.objects.photo.Photo(self.client, {"_illegal_attribute": "test"}) diff --git a/trovebox/objects/trovebox_object.py b/trovebox/objects/trovebox_object.py index 49122ec..86284dc 100644 --- a/trovebox/objects/trovebox_object.py +++ b/trovebox/objects/trovebox_object.py @@ -39,11 +39,11 @@ class TroveboxObject(object): def __repr__(self): if self.name is not None: - return "<%s name='%s'>" % (self.__class__, self.name) + return "<%s name='%s'>" % (self.__class__.__name__, self.name) elif self.id is not None: - return "<%s id='%s'>" % (self.__class__, self.id) + return "<%s id='%s'>" % (self.__class__.__name__, self.id) else: - return "<%s>" % (self.__class__) + return "<%s>" % (self.__class__.__name__) def get_fields(self): """ Returns this object's attributes """ From 121ce12e694be965db217a56440969cc0b5aa198 Mon Sep 17 00:00:00 2001 From: Pete Date: Thu, 29 Aug 2013 14:36:57 -0700 Subject: [PATCH 17/80] Added additional tests for verbosity switch --- tests/unit/test_cli.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 4eb7b2a..dc87c8d 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -106,12 +106,21 @@ class TestCli(unittest.TestCase): @mock.patch.object(trovebox.main.trovebox, "Trovebox") @mock.patch('sys.stdout', new_callable=io.StringIO) - def test_verbose(self, mock_stdout, _): - """Check that the verbose option is working""" + def test_verbose_without_params(self, mock_stdout, _): + """Check that the verbose option works with no parameters""" + main(["-v"]) + self.assertIn("Method: GET", mock_stdout.getvalue()) + self.assertIn("Endpoint: /photos/list.json", mock_stdout.getvalue()) + self.assertNotIn("Fields:", mock_stdout.getvalue()) + + @mock.patch.object(trovebox.main.trovebox, "Trovebox") + @mock.patch('sys.stdout', new_callable=io.StringIO) + def test_verbose_with_params(self, mock_stdout, _): + """Check that the verbose option works with parameters""" main(["-v", "-F foo=bar"]) self.assertIn("Method: GET", mock_stdout.getvalue()) self.assertIn("Endpoint: /photos/list.json", mock_stdout.getvalue()) - self.assertIn("foo=bar", mock_stdout.getvalue()) + self.assertIn("Fields:\n foo=bar", mock_stdout.getvalue()) @mock.patch.object(trovebox.main.trovebox, "Trovebox") @mock.patch('sys.stdout', new_callable=io.StringIO) From f6186d2dbd078e7b285255fb466f0177cead73ae Mon Sep 17 00:00:00 2001 From: Pete Date: Thu, 29 Aug 2013 14:37:33 -0700 Subject: [PATCH 18/80] Add additional tests for http class --- tests/unit/test_http.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/unit/test_http.py b/tests/unit/test_http.py index 79f881b..cfa3cb4 100644 --- a/tests/unit/test_http.py +++ b/tests/unit/test_http.py @@ -112,6 +112,22 @@ class TestHttp(unittest.TestCase): "https://test.example.com/%s" % self.test_endpoint) self.assertEqual(self.client.last_response.json(), self.test_data) + @httpretty.activate + @data(GET, POST) + def test_endpoint_leading_slash(self, method): + """Check that an endpoint with a leading slash is constructed correctly""" + self._register_uri(method, + uri="http://test.example.com/%s" % self.test_endpoint) + + self.client = trovebox.Trovebox(host="http://test.example.com", + **self.test_oauth) + response = GetOrPost(self.client, method).call("/" + self.test_endpoint) + self.assertIn("OAuth", self._last_request().headers["authorization"]) + self.assertEqual(response, self.test_data) + self.assertEqual(self.client.last_url, + "http://test.example.com/%s" % self.test_endpoint) + self.assertEqual(self.client.last_response.json(), self.test_data) + @httpretty.activate def test_get_with_parameters(self): """Check that the get method accepts parameters correctly""" @@ -177,6 +193,7 @@ class TestHttp(unittest.TestCase): self.client.get(self.test_endpoint, photo=photo, album=album, tag=tag, list_=[photo, album, tag], + list2=["1", "2", "3"], boolean=True, unicode_="\xfcmlaut") params = self._last_request().querystring @@ -184,6 +201,7 @@ class TestHttp(unittest.TestCase): self.assertEqual(params["album"], ["album_id"]) self.assertEqual(params["tag"], ["tag_id"]) self.assertEqual(params["list_"], ["photo_id,album_id,tag_id"]) + self.assertEqual(params["list2"], ["1,2,3"]) self.assertEqual(params["boolean"], ["1"]) self.assertIn(params["unicode_"], [["\xc3\xbcmlaut"], ["\xfcmlaut"]]) From ba4515ded677a4a785c8e508d67c01596125d9c9 Mon Sep 17 00:00:00 2001 From: Pete Date: Thu, 29 Aug 2013 14:37:58 -0700 Subject: [PATCH 19/80] Add coverage.py configuration --- .coveragerc | 4 ++++ .gitignore | 2 ++ 2 files changed, 6 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..c379d13 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,4 @@ +# .coveragerc to control coverage.py +[run] +# Capture branch coverage +branch = True diff --git a/.gitignore b/.gitignore index c780865..133ea6d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ dist tests/tokens.py tests.log .tox +.coverage +htmlcov From ca72a307729ff14625cf3b3ed514f1544731c3ac Mon Sep 17 00:00:00 2001 From: Pete Date: Sun, 1 Sep 2013 12:45:48 +0100 Subject: [PATCH 20/80] Add new sub-packages to setup.py. Don't install README.rst. --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 034d3c9..cf58e4b 100755 --- a/setup.py +++ b/setup.py @@ -32,8 +32,7 @@ setup(name='trovebox', long_description=open("README.rst").read(), author='Pete Burgers, James Walker', url='https://github.com/photo/openphoto-python', - packages=['trovebox'], - data_files=['README.rst'], + packages=['trovebox', 'trovebox.objects', 'trovebox.api'], keywords=['openphoto', 'pyopenphoto', 'openphoto-python', 'trovebox', 'pytrovebox', 'trovebox-python'], classifiers=['Development Status :: 4 - Beta', From 380d54c091474318ed8933284c9a1e74aa12eeed Mon Sep 17 00:00:00 2001 From: Pete Date: Sun, 1 Sep 2013 13:23:12 +0100 Subject: [PATCH 21/80] Run coverage as part of test suite. Travis-CI reports coverage results to coveralls.io. --- .travis.yml | 8 +++++++- tox.ini | 10 +++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6891aed..0114cb0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,20 @@ language: python install: + # Install test dependencies - pip install tox --use-mirrors + - pip install coveralls --use-mirrors - .travis/install_pylint script: tox +after_success: + # Send coverage results to coveralls.io + - coveralls + after_script: # Install dependencies for Pylint - - pip install requests requests-oauthlib + - pip install requests requests-oauthlib --use-mirrors # Run Pylint # (for information only, any errors don't affect the Travis result) diff --git a/tox.ini b/tox.ini index 5e0c3da..e552382 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26, py27, py33 +envlist = py26, py27, py33, coverage [testenv] commands = python -m unittest discover --catch tests/unit @@ -16,3 +16,11 @@ deps = ddt >= 0.3.0 unittest2 discover + +[testenv:coverage] +commands = coverage run --source trovebox setup.py test +deps = + mock >= 1.0.0 + httpretty >= 0.6.1 + ddt >= 0.3.0 + coverage From 9b148beeed2c3b83f901859f1e4389849583bd68 Mon Sep 17 00:00:00 2001 From: Pete Date: Sun, 1 Sep 2013 13:33:26 +0100 Subject: [PATCH 22/80] Add coverage badge for upstream master branch --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index c656a89..9449f1c 100644 --- a/README.rst +++ b/README.rst @@ -7,6 +7,10 @@ Trovebox Python Library :alt: Build Status :target: https://travis-ci.org/photo/openphoto-python +.. image:: https://coveralls.io/repos/photo/openphoto-python/badge.png?branch=master + :alt: Coverage Status + :target: https://coveralls.io/r/photo/openphoto-python?branch=master + .. image:: https://pypip.in/v/trovebox/badge.png :alt: Python Package Index (PyPI) :target: https://pypi.python.org/pypi/trovebox From 7159b8b2c0eaf6d660071e2a374f6a1d011cc025 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 1 Sep 2013 14:16:12 +0100 Subject: [PATCH 23/80] Remove coverage badge until master branch is on coveralls.io --- README.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 9449f1c..34a480a 100644 --- a/README.rst +++ b/README.rst @@ -7,9 +7,12 @@ Trovebox Python Library :alt: Build Status :target: https://travis-ci.org/photo/openphoto-python -.. image:: https://coveralls.io/repos/photo/openphoto-python/badge.png?branch=master - :alt: Coverage Status - :target: https://coveralls.io/r/photo/openphoto-python?branch=master +.. + (commented out until master is on coveralls.io) + .. image:: https://coveralls.io/repos/photo/openphoto-python/badge.png?branch=master + :alt: Coverage Status + :target: https://coveralls.io/r/photo/openphoto-python?branch=master +.. .. image:: https://pypip.in/v/trovebox/badge.png :alt: Python Package Index (PyPI) From ec648ea7536e05d6e681c3614f0acb39c31bb344 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 1 Sep 2013 18:10:24 +0100 Subject: [PATCH 24/80] Added HTTPS functional tests --- run_functional_tests | 9 +++++++++ tests/functional/README.markdown | 9 +++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/run_functional_tests b/run_functional_tests index ee3c8fa..c918073 100755 --- a/run_functional_tests +++ b/run_functional_tests @@ -42,3 +42,12 @@ export TROVEBOX_TEST_CONFIG=test-hosted unset TROVEBOX_TEST_SERVER_API python -m unittest discover --catch tests/functional +# Test account on hosted trovebox.com site over HTTPS +tput setaf 3 +echo +echo "Testing latest hosted site over HTTPS..." +tput sgr0 +export TROVEBOX_TEST_CONFIG=test-hosted-https +unset TROVEBOX_TEST_SERVER_API +python -m unittest discover --catch tests/functional + diff --git a/tests/functional/README.markdown b/tests/functional/README.markdown index 5a22abc..6a20718 100644 --- a/tests/functional/README.markdown +++ b/tests/functional/README.markdown @@ -99,7 +99,8 @@ all supported API versions. To use it, you must set up multiple Trovebox instances and create the following config files containing your credentials: - test : Latest self-hosted site (from photo/frontend master branch) - test-apiv1 : APIv1 self-hosted site (from photo/frontend commit 660b2ab) - test-3.0.8 : v3.0.8 self-hosted site (from photo/frontend commit e9d81de57b) - test-hosted : Credentials for test account on trovebox.com + test : Latest self-hosted site (from photo/frontend master branch) + test-apiv1 : APIv1 self-hosted site (from photo/frontend commit 660b2ab) + test-3.0.8 : v3.0.8 self-hosted site (from photo/frontend commit e9d81de57b) + test-hosted : Credentials for test account on http://.trovebox.com + test-hosted-https : Same as test-hosted, but with https:// From 13331efe0d752e977f4b13426a78898a8addeda5 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 1 Sep 2013 18:11:00 +0100 Subject: [PATCH 25/80] Updated functional tests to use new configuration format when specifying API version --- tests/functional/test_base.py | 4 ++-- tests/functional/test_framework.py | 15 +++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/tests/functional/test_base.py b/tests/functional/test_base.py index 8a67d0b..f7e4c25 100644 --- a/tests/functional/test_base.py +++ b/tests/functional/test_base.py @@ -42,8 +42,8 @@ class TestBase(unittest.TestCase): else: print("\nTesting %s v%d" % (cls.testcase_name, cls.api_version)) - cls.client = trovebox.Trovebox(config_file=cls.config_file, - api_version=cls.api_version) + cls.client = trovebox.Trovebox(config_file=cls.config_file) + cls.client.configure(api_version=cls.api_version) if cls.client.photos.list() != []: raise ValueError("The test server (%s) contains photos. " diff --git a/tests/functional/test_framework.py b/tests/functional/test_framework.py index 8495f0d..9444e4b 100644 --- a/tests/functional/test_framework.py +++ b/tests/functional/test_framework.py @@ -16,8 +16,8 @@ class TestFramework(test_base.TestBase): """ API v0 has a special hello world message """ - client = trovebox.Trovebox(config_file=self.config_file, - api_version=0) + client = trovebox.Trovebox(config_file=self.config_file) + client.configure(api_version=0) result = client.get("hello.json") self.assertEqual(result['message'], "Hello, world! This is version zero of the API!") @@ -28,8 +28,8 @@ class TestFramework(test_base.TestBase): For all API versions >0, we get a generic hello world message """ for api_version in range(1, test_base.get_test_server_api() + 1): - client = trovebox.Trovebox(config_file=self.config_file, - api_version=api_version) + client = trovebox.Trovebox(config_file=self.config_file) + client.configure(api_version=api_version) result = client.get("hello.json") self.assertEqual(result['message'], "Hello, world!") self.assertEqual(result['result']['__route__'], @@ -40,8 +40,7 @@ class TestFramework(test_base.TestBase): If the API version is unspecified, we get a generic hello world message. """ - client = trovebox.Trovebox(config_file=self.config_file, - api_version=None) + client = trovebox.Trovebox(config_file=self.config_file) result = client.get("hello.json") self.assertEqual(result['message'], "Hello, world!") self.assertEqual(result['result']['__route__'], "/hello.json") @@ -52,7 +51,7 @@ class TestFramework(test_base.TestBase): (ValueError, since the returned 404 HTML page is not valid JSON) """ version = trovebox.LATEST_API_VERSION + 1 - client = trovebox.Trovebox(config_file=self.config_file, - api_version=version) + client = trovebox.Trovebox(config_file=self.config_file) + client.configure(api_version=version) with self.assertRaises(trovebox.Trovebox404Error): client.get("hello.json") From ba503bea6b5a085261d047a93b42a85e18580f00 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 1 Sep 2013 18:35:28 +0100 Subject: [PATCH 26/80] Whitespace trim --- tests/functional/test_activities.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_activities.py b/tests/functional/test_activities.py index 3c3f68f..a935fa7 100644 --- a/tests/functional/test_activities.py +++ b/tests/functional/test_activities.py @@ -7,9 +7,9 @@ from tests.functional import test_base class TestActivities(test_base.TestBase): testcase_name = "activity API" - + def test_list(self): - """ + """ Upload three photos, and check that three corresponding activities are created. """ @@ -36,7 +36,7 @@ class TestActivities(test_base.TestBase): """ Test that the view endpoint is working correctly """ activity = self.client.activities.list()[0] fields = activity.get_fields().copy() - + # Check that the view method returns the same data as the list activity.view() self.assertEqual(fields, activity.get_fields()) From 016792b65306c321c351fd83a44fdfec9f156d3f Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 1 Sep 2013 18:52:42 +0100 Subject: [PATCH 27/80] Updated functional tests to run Actions and Activities on all API versions --- tests/functional/__init__.py | 2 ++ tests/functional/api_versions/__init__.py | 2 ++ tests/functional/api_versions/test_v1.py | 7 +++++++ tests/functional/api_versions/test_v2.py | 14 +++++++++++++- tests/functional/test_activities.py | 2 ++ 5 files changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/functional/__init__.py b/tests/functional/__init__.py index e69de29..651585a 100644 --- a/tests/functional/__init__.py +++ b/tests/functional/__init__.py @@ -0,0 +1,2 @@ +# __init__.py + diff --git a/tests/functional/api_versions/__init__.py b/tests/functional/api_versions/__init__.py index e69de29..651585a 100644 --- a/tests/functional/api_versions/__init__.py +++ b/tests/functional/api_versions/__init__.py @@ -0,0 +1,2 @@ +# __init__.py + diff --git a/tests/functional/api_versions/test_v1.py b/tests/functional/api_versions/test_v1.py index aa3c652..621602d 100644 --- a/tests/functional/api_versions/test_v1.py +++ b/tests/functional/api_versions/test_v1.py @@ -1,5 +1,12 @@ +from tests.functional import test_activities, test_actions from tests.functional import test_albums, test_photos, test_tags +class TestActivitiesV1(test_activities.TestActivities): + api_version = 1 + +class TestActionsV1(test_actions.TestActions): + api_version = 1 + class TestAlbumsV1(test_albums.TestAlbums): api_version = 1 diff --git a/tests/functional/api_versions/test_v2.py b/tests/functional/api_versions/test_v2.py index a2c425c..80b204e 100644 --- a/tests/functional/api_versions/test_v2.py +++ b/tests/functional/api_versions/test_v2.py @@ -2,7 +2,19 @@ try: import unittest2 as unittest except ImportError: import unittest -from tests.functional import test_base, test_albums, test_photos, test_tags + +from tests.functional import test_base, test_activities, test_actions +from tests.functional import test_albums, test_photos, test_tags + +@unittest.skipIf(test_base.get_test_server_api() < 2, + "Don't test future API versions") +class TestActivitiesV2(test_activities.TestActivities): + api_version = 2 + +@unittest.skipIf(test_base.get_test_server_api() < 2, + "Don't test future API versions") +class TestActionsV2(test_actions.TestActions): + api_version = 2 @unittest.skipIf(test_base.get_test_server_api() < 2, "Don't test future API versions") diff --git a/tests/functional/test_activities.py b/tests/functional/test_activities.py index a935fa7..1475235 100644 --- a/tests/functional/test_activities.py +++ b/tests/functional/test_activities.py @@ -8,6 +8,8 @@ from tests.functional import test_base class TestActivities(test_base.TestBase): testcase_name = "activity API" + @unittest.skipIf(test_base.get_test_server_api() == 1, + "The activity/list endpoint behaves differenty at v1") def test_list(self): """ Upload three photos, and check that three corresponding activities From 6293a81d39da20c8074f1ecdbb4ad321255b01d9 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Mon, 2 Sep 2013 21:19:34 +0100 Subject: [PATCH 28/80] Fix action/create endpoint. --- tests/functional/test_actions.py | 20 +++++++---- tests/unit/test_actions.py | 21 +++++++---- trovebox/.pylint-ignores.patch | 61 ++++++++++++++++++-------------- trovebox/api/api_action.py | 8 ++--- 4 files changed, 65 insertions(+), 45 deletions(-) diff --git a/tests/functional/test_actions.py b/tests/functional/test_actions.py index fce9b89..b4c298a 100644 --- a/tests/functional/test_actions.py +++ b/tests/functional/test_actions.py @@ -3,15 +3,21 @@ try: except ImportError: import unittest +import trovebox from tests.functional import test_base class TestActions(test_base.TestBase): testcase_name = "action API" - # TODO: Enable this test (and write more) once the Actions API is working. - # Currently always returns: - # "Could not find route /action/create.json from /action/create.json" - @unittest.expectedFailure - def test_create_delete(self): - """ Create an action on a photo, then delete it """ - action = self.client.action.create(target=self.photos[0]) + def test_create_view_delete(self): + """ Create an action on a photo, view it, then delete it """ + # Create and check that the action exists + action = self.client.action.create(target=self.photos[0], type="comment", name="test") + action_id = action.id + self.assertEqual(self.client.action.view(action_id).name, "test") + + # Delete and check that the action is gone + action.delete() + with self.assertRaises(trovebox.TroveboxError): + self.client.action.view(action_id) + diff --git a/tests/unit/test_actions.py b/tests/unit/test_actions.py index 3028dbf..6a14006 100644 --- a/tests/unit/test_actions.py +++ b/tests/unit/test_actions.py @@ -14,10 +14,12 @@ class TestActions(unittest.TestCase): test_actions_dict = [{"id": "1", "target": test_photos_dict[0], "target_type": "photo", + "type": "comment", "totalRows": 2}, {"id": "2", "target": test_photos_dict[1], "target_type": "photo", + "type": "comment", "totalRows": 2}] def setUp(self): @@ -36,26 +38,31 @@ class TestActionCreate(TestActions): def test_action_create(self, mock_post): """Check that an action can be created on a photo object""" mock_post.return_value = self._return_value(self.test_actions_dict[0]) - result = self.client.action.create(target=self.test_photos[0], foo="bar") - mock_post.assert_called_with("/action/create.json", target=self.test_photos[0].id, - target_type="photo", + result = self.client.action.create(target=self.test_photos[0], type="comment", foo="bar") + mock_post.assert_called_with("/action/%s/photo/create.json" % + self.test_photos[0].id, + type="comment", foo="bar") self.assertEqual(result.id, "1") self.assertEqual(result.target.id, "photo1") self.assertEqual(result.target_type, "photo") + self.assertEqual(result.type, "comment") @mock.patch.object(trovebox.Trovebox, 'post') def test_action_create_id(self, mock_post): """Check that an action can be created using a photo id""" mock_post.return_value = self._return_value(self.test_actions_dict[0]) - result = self.client.action.create(target=self.test_photos[0].id, - target_type="photo", foo="bar") - mock_post.assert_called_with("/action/create.json", target=self.test_photos[0].id, - target_type="photo", + result = self.client.action.create(target=self.test_photos[0].id, + target_type="photo", type="comment", + foo="bar") + mock_post.assert_called_with("/action/%s/photo/create.json" % + self.test_photos[0].id, + type="comment", foo="bar") self.assertEqual(result.id, "1") self.assertEqual(result.target.id, "photo1") self.assertEqual(result.target_type, "photo") + self.assertEqual(result.type, "comment") @mock.patch.object(trovebox.Trovebox, 'post') def test_action_create_invalid_type(self, mock_post): diff --git a/trovebox/.pylint-ignores.patch b/trovebox/.pylint-ignores.patch index a4d8f5d..3ab31d9 100644 --- a/trovebox/.pylint-ignores.patch +++ b/trovebox/.pylint-ignores.patch @@ -1,6 +1,6 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_activity.py patched/api/api_activity.py ---- original/api/api_activity.py 2013-08-19 17:59:15.592149000 +0100 -+++ patched/api/api_activity.py 2013-08-19 18:08:39.950947589 +0100 +--- original/api/api_activity.py 2013-09-02 21:17:41.848947000 +0100 ++++ patched/api/api_activity.py 2013-09-02 21:18:19.701134833 +0100 @@ -22,7 +22,7 @@ raise TroveboxError("Purge response returned False") return True @@ -11,11 +11,11 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_ac def __init__(self, client): self._client = client diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_album.py patched/api/api_album.py ---- original/api/api_album.py 2013-08-19 16:09:53.539609000 +0100 -+++ patched/api/api_album.py 2013-08-19 18:08:20.118849270 +0100 -@@ -3,7 +3,7 @@ - """ +--- original/api/api_album.py 2013-09-02 21:17:41.848947000 +0100 ++++ patched/api/api_album.py 2013-09-02 21:18:19.701134833 +0100 +@@ -4,7 +4,7 @@ from trovebox.objects.album import Album + from trovebox import http -class ApiAlbums(object): +class ApiAlbums(object): # pylint: disable=R0903 @@ -23,10 +23,10 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_al def __init__(self, client): self._client = client diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_tag.py patched/api/api_tag.py ---- original/api/api_tag.py 2013-08-19 16:09:53.539609000 +0100 -+++ patched/api/api_tag.py 2013-08-19 18:08:20.118849270 +0100 -@@ -3,7 +3,7 @@ - """ +--- original/api/api_tag.py 2013-09-02 21:17:41.848947000 +0100 ++++ patched/api/api_tag.py 2013-09-02 21:18:19.705134853 +0100 +@@ -4,7 +4,7 @@ + from trovebox import http from trovebox.objects.tag import Tag -class ApiTags(object): @@ -35,8 +35,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_ta def __init__(self, client): self._client = client diff --unified --recursive '--exclude=.pylint-ignores.patch' original/auth.py patched/auth.py ---- original/auth.py 2013-08-19 16:09:53.543609000 +0100 -+++ patched/auth.py 2013-08-19 18:08:20.118849270 +0100 +--- original/auth.py 2013-09-02 21:17:41.848947000 +0100 ++++ patched/auth.py 2013-09-02 21:18:19.705134853 +0100 @@ -4,7 +4,7 @@ from __future__ import unicode_literals import os @@ -47,7 +47,7 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/auth.py pa from ConfigParser import SafeConfigParser as ConfigParser # Python2 try: @@ -12,9 +12,9 @@ - except ImportError: + except ImportError: # pragma: no cover import StringIO as io # Python2 -class Auth(object): @@ -68,8 +68,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/auth.py pa parser.readfp(buf) # Python2 diff --unified --recursive '--exclude=.pylint-ignores.patch' original/http.py patched/http.py ---- original/http.py 2013-08-19 16:09:53.543609000 +0100 -+++ patched/http.py 2013-08-19 18:08:20.118849270 +0100 +--- original/http.py 2013-09-02 21:17:41.848947000 +0100 ++++ patched/http.py 2013-09-02 21:18:25.749164824 +0100 @@ -7,18 +7,18 @@ import requests_oauthlib import logging @@ -87,7 +87,7 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/http.py pa if sys.version < '3': - TEXT_TYPE = unicode + TEXT_TYPE = unicode # pylint: disable=C0103 - else: + else: # pragma: no cover - TEXT_TYPE = str + TEXT_TYPE = str # pylint: disable=C0103 @@ -103,8 +103,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/http.py pa token='', token_secret='', api_version=None): diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.py patched/__init__.py ---- original/__init__.py 2013-08-19 17:02:22.951226000 +0100 -+++ patched/__init__.py 2013-08-19 18:08:36.194928993 +0100 +--- original/__init__.py 2013-09-02 21:17:41.848947000 +0100 ++++ patched/__init__.py 2013-09-02 21:18:19.705134853 +0100 @@ -2,7 +2,7 @@ __init__.py : Trovebox package top level """ @@ -133,8 +133,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.p token='', token_secret='', api_version=None): diff --unified --recursive '--exclude=.pylint-ignores.patch' original/main.py patched/main.py ---- original/main.py 2013-08-19 16:09:53.543609000 +0100 -+++ patched/main.py 2013-08-19 18:08:20.118849270 +0100 +--- original/main.py 2013-09-02 21:17:41.852947000 +0100 ++++ patched/main.py 2013-09-02 21:18:19.705134853 +0100 @@ -26,7 +26,7 @@ ################################################################# @@ -161,9 +161,18 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/main.py pa files[f].close() if options.verbose: +diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/photo.py patched/objects/photo.py +--- original/objects/photo.py 2013-09-02 19:48:44.862482000 +0100 ++++ patched/objects/photo.py 2013-09-02 21:18:29.001180950 +0100 +@@ -1,4 +1,4 @@ +-""" ++""" # pylint: disable=R0801 + Representation of a Photo object + """ + from trovebox.errors import TroveboxError diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/tag.py patched/objects/tag.py ---- original/objects/tag.py 2013-08-19 16:09:53.543609000 +0100 -+++ patched/objects/tag.py 2013-08-19 18:08:20.118849270 +0100 +--- original/objects/tag.py 2013-09-02 21:17:41.852947000 +0100 ++++ patched/objects/tag.py 2013-09-02 21:18:19.705134853 +0100 @@ -1,8 +1,8 @@ -""" +""" # pylint: disable=R0801 @@ -176,8 +185,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/ta from urllib import quote # Python2 diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/trovebox_object.py patched/objects/trovebox_object.py ---- original/objects/trovebox_object.py 2013-08-19 16:09:53.543609000 +0100 -+++ patched/objects/trovebox_object.py 2013-08-19 18:08:20.118849270 +0100 +--- original/objects/trovebox_object.py 2013-09-02 21:17:41.852947000 +0100 ++++ patched/objects/trovebox_object.py 2013-09-02 21:18:19.705134853 +0100 @@ -1,10 +1,10 @@ """ Base object supporting the storage of custom fields as attributes @@ -192,8 +201,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/tr self._trovebox = trovebox self._json_dict = json_dict diff --unified --recursive '--exclude=.pylint-ignores.patch' original/_version.py patched/_version.py ---- original/_version.py 2013-08-19 16:09:53.543609000 +0100 -+++ patched/_version.py 2013-08-19 18:08:20.118849270 +0100 +--- original/_version.py 2013-09-02 21:17:41.852947000 +0100 ++++ patched/_version.py 2013-09-02 21:18:19.705134853 +0100 @@ -1,2 +1,2 @@ - + # pylint: disable=C0111 diff --git a/trovebox/api/api_action.py b/trovebox/api/api_action.py index 1712a8a..a0f586c 100644 --- a/trovebox/api/api_action.py +++ b/trovebox/api/api_action.py @@ -20,9 +20,7 @@ class ApiAction(object): if isinstance(target, Photo): target_type = "photo" else: - raise NotImplementedError("Actions can only be assigned to " - "Photos when target_type isn't " - "specified") + raise NotImplementedError("Unsupported target type") # Extract the ID from the target try: target_id = target.id @@ -30,8 +28,8 @@ class ApiAction(object): # Assume the ID was passed in directly target_id = target - result = self._client.post("/action/create.json", - target=target_id, target_type=target_type, + result = self._client.post("/action/%s/%s/create.json" % + (target_id, target_type), **kwds)["result"] return Action(self._client, result) From 0f6cbd58e06cfbc2af39b683b4294c6f02bf5a00 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Mon, 2 Sep 2013 22:00:15 +0100 Subject: [PATCH 29/80] Create API base class --- trovebox/api/api_action.py | 6 ++---- trovebox/api/api_album.py | 11 +++-------- trovebox/api/api_base.py | 8 ++++++++ trovebox/api/api_photo.py | 11 +++-------- trovebox/api/api_tag.py | 11 +++-------- 5 files changed, 19 insertions(+), 28 deletions(-) create mode 100644 trovebox/api/api_base.py diff --git a/trovebox/api/api_action.py b/trovebox/api/api_action.py index a0f586c..f6b571e 100644 --- a/trovebox/api/api_action.py +++ b/trovebox/api/api_action.py @@ -3,12 +3,10 @@ api_action.py : Trovebox Action API Classes """ from trovebox.objects.action import Action from trovebox.objects.photo import Photo +from .api_base import ApiBase -class ApiAction(object): +class ApiAction(ApiBase): """ Definitions of /action/ API endpoints """ - def __init__(self, client): - self._client = client - def create(self, target, target_type=None, **kwds): """ Create a new action and return it. diff --git a/trovebox/api/api_album.py b/trovebox/api/api_album.py index 4326816..f767c8f 100644 --- a/trovebox/api/api_album.py +++ b/trovebox/api/api_album.py @@ -3,23 +3,18 @@ api_album.py : Trovebox Album API Classes """ from trovebox.objects.album import Album from trovebox import http +from .api_base import ApiBase -class ApiAlbums(object): +class ApiAlbums(ApiBase): """ Definitions of /albums/ API endpoints """ - def __init__(self, client): - self._client = client - def list(self, **kwds): """ Return a list of Album objects """ albums = self._client.get("/albums/list.json", **kwds)["result"] albums = http.result_to_list(albums) return [Album(self._client, album) for album in albums] -class ApiAlbum(object): +class ApiAlbum(ApiBase): """ Definitions of /album/ API endpoints """ - def __init__(self, client): - self._client = client - def create(self, name, **kwds): """ Create a new album and return it""" result = self._client.post("/album/create.json", diff --git a/trovebox/api/api_base.py b/trovebox/api/api_base.py new file mode 100644 index 0000000..3e906be --- /dev/null +++ b/trovebox/api/api_base.py @@ -0,0 +1,8 @@ +""" +api_base.py: Base class for all API classes +""" + +class ApiBase(object): + def __init__(self, client): + self._client = client + diff --git a/trovebox/api/api_photo.py b/trovebox/api/api_photo.py index b5f8512..e83e74e 100644 --- a/trovebox/api/api_photo.py +++ b/trovebox/api/api_photo.py @@ -6,6 +6,7 @@ import base64 from trovebox import http from trovebox.errors import TroveboxError from trovebox.objects.photo import Photo +from .api_base import ApiBase def extract_ids(photos): """ @@ -20,11 +21,8 @@ def extract_ids(photos): ids.append(photo) return ids -class ApiPhotos(object): +class ApiPhotos(ApiBase): """ Definitions of /photos/ API endpoints """ - def __init__(self, client): - self._client = client - def list(self, **kwds): """ Returns a list of Photo objects """ photos = self._client.get("/photos/list.json", **kwds)["result"] @@ -55,11 +53,8 @@ class ApiPhotos(object): raise TroveboxError("Delete response returned False") return True -class ApiPhoto(object): +class ApiPhoto(ApiBase): """ Definitions of /photo/ API endpoints """ - def __init__(self, client): - self._client = client - def delete(self, photo, **kwds): """ Delete a photo. diff --git a/trovebox/api/api_tag.py b/trovebox/api/api_tag.py index acd6fca..dd11b15 100644 --- a/trovebox/api/api_tag.py +++ b/trovebox/api/api_tag.py @@ -3,23 +3,18 @@ api_tag.py : Trovebox Tag API Classes """ from trovebox import http from trovebox.objects.tag import Tag +from .api_base import ApiBase -class ApiTags(object): +class ApiTags(ApiBase): """ Definitions of /tags/ API endpoints """ - def __init__(self, client): - self._client = client - def list(self, **kwds): """ Returns a list of Tag objects """ tags = self._client.get("/tags/list.json", **kwds)["result"] tags = http.result_to_list(tags) return [Tag(self._client, tag) for tag in tags] -class ApiTag(object): +class ApiTag(ApiBase): """ Definitions of /tag/ API endpoints """ - def __init__(self, client): - self._client = client - def create(self, tag, **kwds): """ Create a new tag. From c39c4f3d8ca3e4706fc69e4f344f0aa53ec8aef4 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Thu, 5 Sep 2013 22:51:32 +0100 Subject: [PATCH 30/80] Add support for activity list filters --- tests/functional/test_activities.py | 19 ++++++++++++++++++- tests/unit/test_activities.py | 11 +++++++++++ trovebox/api/api_activity.py | 17 +++++++---------- trovebox/api/api_base.py | 10 ++++++++++ 4 files changed, 46 insertions(+), 11 deletions(-) diff --git a/tests/functional/test_activities.py b/tests/functional/test_activities.py index 1475235..e19ada9 100644 --- a/tests/functional/test_activities.py +++ b/tests/functional/test_activities.py @@ -21,10 +21,27 @@ class TestActivities(test_base.TestBase): # Check that each activity is for a valid test photo activities = self.client.activities.list() - self.assertEqual(len(activities), len(self.photos)) + self.assertEqual(len(activities), len(photos)) for activity in activities: self.assertIn(activity.data.id, [photo.id for photo in photos]) + def test_list_filter(self): + """ + Check that the activity list filter parameter works correctly + """ + self._delete_all() + self._create_test_photos(tag=False) + photos = self.client.photos.list() + + # Dummy photo update activity + photos[0].update(tags=photos[0].tags) + + # Check that the activities can be filtered + upload_activities = self.client.activities.list(filters={"type": "photo-upload"}) + update_activities = self.client.activities.list(filters={"type": "photo-update"}) + self.assertEqual(len(upload_activities), len(photos)) + self.assertEqual(len(update_activities), 1) + # The purge endpoint currently reports a 500: Internal Server Error @unittest.expectedFailure def test_purge(self): diff --git a/tests/unit/test_activities.py b/tests/unit/test_activities.py index 0ea190f..29e3afa 100644 --- a/tests/unit/test_activities.py +++ b/tests/unit/test_activities.py @@ -68,6 +68,17 @@ class TestActivitiesList(TestActivities): mock_get.assert_called_with("/activities/list.json") self.assertEqual(result, []) + @mock.patch.object(trovebox.Trovebox, 'get') + def test_filters(self, mock_get): + """Check that the activity list filters are applied properly""" + mock_get.return_value = self._return_value(self.test_activities_dict) + self.client.activities.list(filters={"foo": "bar", + "test1": "test2"}) + # Dict element can be any order + self.assertIn(mock_get.call_args[0], + [("/activities/foo-bar/test1-test2/list.json",), + ("/activities/test1-test2/foo-bar/list.json",)]) + class TestActivitiesPurge(TestActivities): @mock.patch.object(trovebox.Trovebox, 'post') def test_activity_purge(self, mock_get): diff --git a/trovebox/api/api_activity.py b/trovebox/api/api_activity.py index 0994cd0..4c745e2 100644 --- a/trovebox/api/api_activity.py +++ b/trovebox/api/api_activity.py @@ -4,15 +4,15 @@ api_activity.py : Trovebox Activity API Classes from trovebox import http from trovebox.errors import TroveboxError from trovebox.objects.activity import Activity +from .api_base import ApiBase -class ApiActivities(object): +class ApiActivities(ApiBase): """ Definitions of /activities/ API endpoints """ - def __init__(self, client): - self._client = client - - def list(self, **kwds): + def list(self, filters={}, **kwds): """ Returns a list of Activity objects """ - activities = self._client.get("/activities/list.json", **kwds)["result"] + filter_string = self._build_filter_string(filters) + activities = self._client.get("/activities/%slist.json" % filter_string, + **kwds)["result"] activities = http.result_to_list(activities) return [Activity(self._client, activity) for activity in activities] @@ -22,11 +22,8 @@ class ApiActivities(object): raise TroveboxError("Purge response returned False") return True -class ApiActivity(object): +class ApiActivity(ApiBase): """ Definitions of /activity/ API endpoints """ - def __init__(self, client): - self._client = client - def view(self, activity, **kwds): """ View an activity's contents. diff --git a/trovebox/api/api_base.py b/trovebox/api/api_base.py index 3e906be..ef462bd 100644 --- a/trovebox/api/api_base.py +++ b/trovebox/api/api_base.py @@ -6,3 +6,13 @@ class ApiBase(object): def __init__(self, client): self._client = client + @staticmethod + def _build_filter_string(filters): + """ + :param filters: dictionary containing the filters + :returns: filter_string formatted for an API endpoint + """ + filter_string = "" + for filter in filters: + filter_string += "%s-%s/" % (filter, filters[filter]) + return filter_string From e8806426cec3dfaf020fd13a7870801b129e1b6f Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Fri, 6 Sep 2013 18:20:03 +0100 Subject: [PATCH 31/80] Use DDT to simplify http_error testcases --- tests/unit/test_http_errors.py | 143 ++++++++++----------------------- 1 file changed, 41 insertions(+), 102 deletions(-) diff --git a/tests/unit/test_http_errors.py b/tests/unit/test_http_errors.py index 4a1845b..a4ec376 100644 --- a/tests/unit/test_http_errors.py +++ b/tests/unit/test_http_errors.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals import json import httpretty +from httpretty import GET, POST +from ddt import ddt, data # TEMP: Temporary hack until httpretty string checking is fixed if httpretty.compat.PY3: @@ -11,8 +13,10 @@ try: except ImportError: import unittest +from test_http import GetOrPost import trovebox +@ddt class TestHttpErrors(unittest.TestCase): test_host = "test.example.com" test_endpoint = "test.json" @@ -42,146 +46,81 @@ class TestHttpErrors(unittest.TestCase): **kwds) @httpretty.activate - def test_get_with_error_status(self): + @data(GET, POST) + def test_error_status(self, method): """ - Check that an error status causes the get method + Check that an error status causes the get/post methods to raise an exception """ - self._register_uri(httpretty.GET, status=500) + self._register_uri(method, status=500) with self.assertRaises(trovebox.TroveboxError): - self.client.get(self.test_endpoint) + GetOrPost(self.client, method).call(self.test_endpoint) @httpretty.activate - def test_post_with_error_status(self): + @data(GET, POST) + def test_404_status(self, method): """ - Check that an error status causes the post method - to raise an exception - """ - self._register_uri(httpretty.POST, status=500) - with self.assertRaises(trovebox.TroveboxError): - self.client.post(self.test_endpoint) - - @httpretty.activate - def test_get_with_404_status(self): - """ - Check that a 404 status causes the get method + Check that a 404 status causes the get/post methods to raise a 404 exception """ - self._register_uri(httpretty.GET, status=404) + self._register_uri(method, status=404) with self.assertRaises(trovebox.Trovebox404Error): - self.client.get(self.test_endpoint) + GetOrPost(self.client, method).call(self.test_endpoint) @httpretty.activate - def test_post_with_404_status(self): + @data(GET, POST) + def test_with_invalid_json(self, method): """ - Check that a 404 status causes the post method - to raise a 404 exception - """ - self._register_uri(httpretty.POST, status=404) - with self.assertRaises(trovebox.Trovebox404Error): - self.client.post(self.test_endpoint) - - @httpretty.activate - def test_get_with_invalid_json(self): - """ - Check that invalid JSON causes the get method to + Check that invalid JSON causes the get/post methods to raise an exception """ - self._register_uri(httpretty.GET, body="Invalid JSON") + self._register_uri(method, body="Invalid JSON") with self.assertRaises(ValueError): - self.client.get(self.test_endpoint) + GetOrPost(self.client, method).call(self.test_endpoint) @httpretty.activate - def test_post_with_invalid_json(self): + @data(GET, POST) + def test_with_error_status_and_invalid_json(self, method): """ - Check that invalid JSON causes the post method to - raise an exception + Check that invalid JSON causes the get/post methods to raise + an exception, even with an error status is returned """ - self._register_uri(httpretty.POST, body="Invalid JSON") - with self.assertRaises(ValueError): - self.client.post(self.test_endpoint) - - @httpretty.activate - def test_get_with_error_status_and_invalid_json(self): - """ - Check that invalid JSON causes the get method to raise an exception, - even with an error status is returned - """ - self._register_uri(httpretty.GET, body="Invalid JSON", status=500) + self._register_uri(method, body="Invalid JSON", status=500) with self.assertRaises(trovebox.TroveboxError): - self.client.get(self.test_endpoint) + GetOrPost(self.client, method).call(self.test_endpoint) @httpretty.activate - def test_post_with_error_status_and_invalid_json(self): + @data(GET, POST) + def test_with_404_status_and_invalid_json(self, method): """ - Check that invalid JSON causes the post method to raise an exception, - even with an error status is returned + Check that invalid JSON causes the get/post methods to raise + an exception, even with a 404 status is returned """ - self._register_uri(httpretty.POST, body="Invalid JSON", status=500) - with self.assertRaises(trovebox.TroveboxError): - self.client.post(self.test_endpoint) - - @httpretty.activate - def test_get_with_404_status_and_invalid_json(self): - """ - Check that invalid JSON causes the get method to raise an exception, - even with a 404 status is returned - """ - self._register_uri(httpretty.GET, body="Invalid JSON", status=404) + self._register_uri(method, body="Invalid JSON", status=404) with self.assertRaises(trovebox.Trovebox404Error): - self.client.get(self.test_endpoint) + GetOrPost(self.client, method).call(self.test_endpoint) @httpretty.activate - def test_post_with_404_status_and_invalid_json(self): + @data(GET, POST) + def test_with_duplicate_status(self, method): """ - Check that invalid JSON causes the post method to raise an exception, - even with a 404 status is returned - """ - self._register_uri(httpretty.POST, body="Invalid JSON", status=404) - with self.assertRaises(trovebox.Trovebox404Error): - self.client.post(self.test_endpoint) - - @httpretty.activate - def test_get_with_duplicate_status(self): - """ - Check that a get with a duplicate status + Check that a get/post with a duplicate status raises a duplicate exception """ data = {"message": "This photo already exists", "code": 409} - self._register_uri(httpretty.GET, data=data, status=409) + self._register_uri(method, data=data, status=409) with self.assertRaises(trovebox.TroveboxDuplicateError): - self.client.get(self.test_endpoint) + GetOrPost(self.client, method).call(self.test_endpoint) @httpretty.activate - def test_post_with_duplicate_status(self): - """ - Check that a post with a duplicate status - raises a duplicate exception - """ - data = {"message": "This photo already exists", "code": 409} - self._register_uri(httpretty.POST, data=data, status=409) - with self.assertRaises(trovebox.TroveboxDuplicateError): - self.client.post(self.test_endpoint) - - @httpretty.activate - def test_get_with_status_code_mismatch(self): + @data(GET, POST) + def test_with_status_code_mismatch(self, method): """ Check that a mismatched HTTP status code still returns the - JSON status code for get requests. + JSON status code. """ data = {"message": "Test Message", "code": 202} - self._register_uri(httpretty.GET, data=data, status=200) - response = self.client.get(self.test_endpoint) - self.assertEqual(response["code"], 202) - - @httpretty.activate - def test_post_with_status_code_mismatch(self): - """ - Check that a mismatched HTTP status code still returns the - JSON status code for post requests. - """ - data = {"message": "Test Message", "code": 202} - self._register_uri(httpretty.POST, data=data, status=200) - response = self.client.post(self.test_endpoint) + self._register_uri(method, data=data, status=200) + response = GetOrPost(self.client, method).call(self.test_endpoint) self.assertEqual(response["code"], 202) From 533c2661b18f2e6032eb713049749d7f1f12a482 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Fri, 6 Sep 2013 18:21:21 +0100 Subject: [PATCH 32/80] Ensure HTTP errors raise exceptions even when response processing is disabled --- tests/unit/test_http_errors.py | 12 ++++++++++++ trovebox/http.py | 12 ++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_http_errors.py b/tests/unit/test_http_errors.py index a4ec376..f2a7893 100644 --- a/tests/unit/test_http_errors.py +++ b/tests/unit/test_http_errors.py @@ -124,3 +124,15 @@ class TestHttpErrors(unittest.TestCase): response = GetOrPost(self.client, method).call(self.test_endpoint) self.assertEqual(response["code"], 202) + @httpretty.activate + @data(GET, POST) + def test_http_error_with_no_response_processing(self, method): + """ + Check that get/post methods work with response processing disabled + when an HTTP error code is returned. + """ + httpretty.register_uri(method, self.test_uri, status=500) + with self.assertRaises(trovebox.TroveboxError): + response = GetOrPost(self.client, method).call(self.test_endpoint, + process_response=False) + diff --git a/trovebox/http.py b/trovebox/http.py index dd33917..ffeb487 100644 --- a/trovebox/http.py +++ b/trovebox/http.py @@ -113,7 +113,11 @@ class Http(object): if process_response: return self._process_response(response) else: - return response.text + if 200 <= response.status_code < 300: + return response.text + else: + raise TroveboxError("HTTP Error %d: %s" % + (response.status_code, response.reason)) def post(self, endpoint, process_response=True, files=None, **params): """ @@ -163,7 +167,11 @@ class Http(object): if process_response: return self._process_response(response) else: - return response.text + if 200 <= response.status_code < 300: + return response.text + else: + raise TroveboxError("HTTP Error %d: %s" % + (response.status_code, response.reason)) def _construct_url(self, endpoint): """Return the full URL to the specified endpoint""" From c598a68017a1c703e19e8aade71d37e02a4d0516 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 7 Sep 2013 09:12:55 +0100 Subject: [PATCH 33/80] Finished activities/purge test, fails due to frontend/#1368 --- tests/functional/test_activities.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/functional/test_activities.py b/tests/functional/test_activities.py index e19ada9..4f7bbf7 100644 --- a/tests/functional/test_activities.py +++ b/tests/functional/test_activities.py @@ -43,12 +43,18 @@ class TestActivities(test_base.TestBase): self.assertEqual(len(update_activities), 1) # The purge endpoint currently reports a 500: Internal Server Error + # PHP Fatal error: + # Call to undefined method DatabaseMySql::postActivitiesPurge() + # in /var/www/openphoto-master/src/libraries/models/Activity.php + # on line 66 + # Tracked in frontend/#1368 @unittest.expectedFailure def test_purge(self): """ Test that the purge endpoint deletes all activities """ activities = self.client.activities.list() self.assertNotEqual(activities, []) self.client.activities.purge() + activities = self.client.activities.list() self.assertEqual(activities, []) def test_view(self): From 3e4fdf6dd69d42bf762e2667e0d8e859405592c8 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 7 Sep 2013 09:42:29 +0100 Subject: [PATCH 34/80] Docstring updates --- trovebox/api/api_action.py | 17 ++++++++++++----- trovebox/api/api_activity.py | 19 ++++++++++++++++--- trovebox/api/api_album.py | 20 ++++++++++++++++---- trovebox/objects/action.py | 6 +++++- trovebox/objects/activity.py | 2 ++ trovebox/objects/album.py | 14 +++++++++++--- 6 files changed, 62 insertions(+), 16 deletions(-) diff --git a/trovebox/api/api_action.py b/trovebox/api/api_action.py index f6b571e..f452540 100644 --- a/trovebox/api/api_action.py +++ b/trovebox/api/api_action.py @@ -9,9 +9,12 @@ class ApiAction(ApiBase): """ Definitions of /action/ API endpoints """ def create(self, target, target_type=None, **kwds): """ - Create a new action and return it. - If the target_type parameter isn't specified, it is automatically - generated. + Endpoint: /action///create.json + + Creates a new action and returns it. + The target parameter can either be an id or a Trovebox object. + If a Trovebox object is used, the target type is inferred + automatically. """ if target_type is None: # Determine the target type @@ -33,7 +36,9 @@ class ApiAction(ApiBase): def delete(self, action, **kwds): """ - Delete an action. + Endpoint: /action//delete.json + + Deletes an action. Returns True if successful. Raises a TroveboxError if not. """ @@ -43,7 +48,9 @@ class ApiAction(ApiBase): def view(self, action, **kwds): """ - View an action's contents. + Endpoint: /action//view.json + + Requests all properties of an action. Returns the requested action object. """ if not isinstance(action, Action): diff --git a/trovebox/api/api_activity.py b/trovebox/api/api_activity.py index 4c745e2..2d68f2b 100644 --- a/trovebox/api/api_activity.py +++ b/trovebox/api/api_activity.py @@ -9,7 +9,13 @@ from .api_base import ApiBase class ApiActivities(ApiBase): """ Definitions of /activities/ API endpoints """ def list(self, filters={}, **kwds): - """ Returns a list of Activity objects """ + """ + Endpoint: /activities/[]/list.json + + Returns a list of Activity objects. + The filters parameter can be used to narrow down the returned activities. + Eg: filters={"type": "photo-upload"} + """ filter_string = self._build_filter_string(filters) activities = self._client.get("/activities/%slist.json" % filter_string, **kwds)["result"] @@ -17,7 +23,12 @@ class ApiActivities(ApiBase): return [Activity(self._client, activity) for activity in activities] def purge(self, **kwds): - """ Purge all activities """ + """ + Endpoint: /activities/purge.json + + Purges all activities. + Currently not working due to frontend issue #1368 + """ if not self._client.post("/activities/purge.json", **kwds)["result"]: raise TroveboxError("Purge response returned False") return True @@ -26,7 +37,9 @@ class ApiActivity(ApiBase): """ Definitions of /activity/ API endpoints """ def view(self, activity, **kwds): """ - View an activity's contents. + Endpoint: /activity//view.json + + Requests all properties of an activity. Returns the requested activity object. """ if not isinstance(activity, Activity): diff --git a/trovebox/api/api_album.py b/trovebox/api/api_album.py index f767c8f..e46eeea 100644 --- a/trovebox/api/api_album.py +++ b/trovebox/api/api_album.py @@ -16,14 +16,20 @@ class ApiAlbums(ApiBase): class ApiAlbum(ApiBase): """ Definitions of /album/ API endpoints """ def create(self, name, **kwds): - """ Create a new album and return it""" + """ + Endpoint: /album/create.json + + Creates a new album and returns it. + """ result = self._client.post("/album/create.json", name=name, **kwds)["result"] return Album(self._client, result) def delete(self, album, **kwds): """ - Delete an album. + Endpoint: /album//delete.json + + Deletes an album. Returns True if successful. Raises a TroveboxError if not. """ @@ -44,7 +50,11 @@ class ApiAlbum(ApiBase): raise NotImplementedError() def update(self, album, **kwds): - """ Update an album """ + """ + Endpoint: /album//update.json + + Updates an album with the specified parameters. + """ if not isinstance(album, Album): album = Album(self._client, {"id": album}) album.update(**kwds) @@ -52,7 +62,9 @@ class ApiAlbum(ApiBase): def view(self, album, **kwds): """ - View an album's contents. + Endpoint: /album//view.json + + Requests all properties of an album. Returns the requested album object. """ if not isinstance(album, Album): diff --git a/trovebox/objects/action.py b/trovebox/objects/action.py index c15af37..e7c9ce3 100644 --- a/trovebox/objects/action.py +++ b/trovebox/objects/action.py @@ -25,7 +25,9 @@ class Action(TroveboxObject): def delete(self, **kwds): """ - Delete this action. + Endpoint: /action//delete.json + + Deletes this action. Returns True if successful. Raises a TroveboxError if not. """ @@ -38,6 +40,8 @@ class Action(TroveboxObject): def view(self, **kwds): """ + Endpoint: /action//view.json + Requests the full contents of the action. Updates the action's fields with the response. """ diff --git a/trovebox/objects/activity.py b/trovebox/objects/activity.py index fe57013..fae7605 100644 --- a/trovebox/objects/activity.py +++ b/trovebox/objects/activity.py @@ -26,6 +26,8 @@ class Activity(TroveboxObject): def view(self, **kwds): """ + Endpoint: /activity//view.json + Requests the full contents of the activity. Updates the activity's fields with the response. """ diff --git a/trovebox/objects/album.py b/trovebox/objects/album.py index 62b898c..b2acdb4 100644 --- a/trovebox/objects/album.py +++ b/trovebox/objects/album.py @@ -20,7 +20,9 @@ class Album(TroveboxObject): def delete(self, **kwds): """ - Delete this album. + Endpoint: /album//delete.json + + Deletes this album. Returns True if successful. Raises a TroveboxError if not. """ @@ -44,7 +46,11 @@ class Album(TroveboxObject): raise NotImplementedError() def update(self, **kwds): - """ Update this album with the specified parameters """ + """ + Endpoint: /album//update.json + + Updates this album with the specified parameters. + """ result = self._trovebox.post("/album/%s/update.json" % self.id, **kwds)["result"] @@ -58,7 +64,9 @@ class Album(TroveboxObject): def view(self, **kwds): """ - Requests the full contents of the album. + Endpoint: /album//view.json + + Requests all properties of an album. Updates the album's fields with the response. """ result = self._trovebox.get("/album/%s/view.json" % From 7afb709531e11847c4fa807a72045f46858e7082 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 7 Sep 2013 10:49:14 +0100 Subject: [PATCH 35/80] Update docstrings --- trovebox/api/api_album.py | 11 ++++- trovebox/api/api_photo.py | 90 ++++++++++++++++++++++++++++----------- trovebox/objects/album.py | 4 ++ trovebox/objects/photo.py | 35 ++++++++++++--- 4 files changed, 108 insertions(+), 32 deletions(-) diff --git a/trovebox/api/api_album.py b/trovebox/api/api_album.py index e46eeea..eaa147b 100644 --- a/trovebox/api/api_album.py +++ b/trovebox/api/api_album.py @@ -8,13 +8,19 @@ from .api_base import ApiBase class ApiAlbums(ApiBase): """ Definitions of /albums/ API endpoints """ def list(self, **kwds): - """ Return a list of Album objects """ + """ + Endpoint: /albums/list.json + + Returns a list of Album objects. + """ albums = self._client.get("/albums/list.json", **kwds)["result"] albums = http.result_to_list(albums) return [Album(self._client, album) for album in albums] class ApiAlbum(ApiBase): """ Definitions of /album/ API endpoints """ + # def cover_update(self, album, photo, **kwds): + def create(self, name, **kwds): """ Endpoint: /album/create.json @@ -41,10 +47,12 @@ class ApiAlbum(ApiBase): """ Not yet implemented """ raise NotImplementedError() + # TODO: Should be just "add" def add_photos(self, album, photos, **kwds): """ Not yet implemented """ raise NotImplementedError() + # TODO: Should be just "remove" def remove_photos(self, album, photos, **kwds): """ Not yet implemented """ raise NotImplementedError() @@ -54,6 +62,7 @@ class ApiAlbum(ApiBase): Endpoint: /album//update.json Updates an album with the specified parameters. + Returns the updated album object. """ if not isinstance(album, Album): album = Album(self._client, {"id": album}) diff --git a/trovebox/api/api_photo.py b/trovebox/api/api_photo.py index e83e74e..885ac61 100644 --- a/trovebox/api/api_photo.py +++ b/trovebox/api/api_photo.py @@ -8,7 +8,7 @@ from trovebox.errors import TroveboxError from trovebox.objects.photo import Photo from .api_base import ApiBase -def extract_ids(photos): +def _extract_ids(photos): """ Given a list of objects, extract the photo id for each Photo object. @@ -23,41 +23,54 @@ def extract_ids(photos): class ApiPhotos(ApiBase): """ Definitions of /photos/ API endpoints """ + # TODO: Add options def list(self, **kwds): - """ Returns a list of Photo objects """ + """ + Endpoint: /photos/list.json + + Returns a list of Photo objects. + """ photos = self._client.get("/photos/list.json", **kwds)["result"] photos = http.result_to_list(photos) return [Photo(self._client, photo) for photo in photos] - def update(self, photos, **kwds): - """ - Updates a list of photos. - Returns True if successful. - Raises TroveboxError if not. - """ - ids = extract_ids(photos) - if not self._client.post("/photos/update.json", ids=ids, - **kwds)["result"]: - raise TroveboxError("Update response returned False") - return True + # def share(self, **kwds): def delete(self, photos, **kwds): """ + Endpoint: /photos/delete.json + Deletes a list of photos. Returns True if successful. - Raises TroveboxError if not. + Raises a TroveboxError if not. """ - ids = extract_ids(photos) + ids = _extract_ids(photos) if not self._client.post("/photos/delete.json", ids=ids, **kwds)["result"]: raise TroveboxError("Delete response returned False") return True + def update(self, photos, **kwds): + """ + Endpoint: /photos//update.json + + Updates a list of photos with the specified parameters. + Returns True if successful. + Raises TroveboxError if not. + """ + ids = _extract_ids(photos) + if not self._client.post("/photos/update.json", ids=ids, + **kwds)["result"]: + raise TroveboxError("Update response returned False") + return True + class ApiPhoto(ApiBase): """ Definitions of /photo/ API endpoints """ def delete(self, photo, **kwds): """ - Delete a photo. + Endpoint: /photo//delete.json + + Deletes a photo. Returns True if successful. Raises a TroveboxError if not. """ @@ -65,8 +78,14 @@ class ApiPhoto(ApiBase): photo = Photo(self._client, {"id": photo}) return photo.delete(**kwds) + # def delete_source(self, photo, **kwds): + def edit(self, photo, **kwds): - """ Returns an HTML form to edit a photo """ + """ + Endpoint: /photo//edit.json + + Returns an HTML form to edit a photo's attributes. + """ if not isinstance(photo, Photo): photo = Photo(self._client, {"id": photo}) return photo.edit(**kwds) @@ -81,18 +100,25 @@ class ApiPhoto(ApiBase): def update(self, photo, **kwds): """ - Update a photo with the specified parameters. - Returns the updated photo object + Endpoint: /photo//update.json + + Updates a photo with the specified parameters. + Returns the updated photo object. """ if not isinstance(photo, Photo): photo = Photo(self._client, {"id": photo}) photo.update(**kwds) return photo + # TODO: Add options def view(self, photo, **kwds): """ - Used to view the photo at a particular size. - Returns the requested photo object + Endpoint: /photo//view.json + + Requests all properties of a photo. + Can be used to obtain URLs for the photo at a particular size, + by using the "returnSizes" parameter. + Returns the requested photo object. """ if not isinstance(photo, Photo): photo = Photo(self._client, {"id": photo}) @@ -100,7 +126,11 @@ class ApiPhoto(ApiBase): return photo def upload(self, photo_file, **kwds): - """ Uploads the specified file to the server """ + """ + Endpoint: /photo/upload.json + + Uploads the specified photo filename. + """ with open(photo_file, 'rb') as in_file: result = self._client.post("/photo/upload.json", files={'photo': in_file}, @@ -108,7 +138,11 @@ class ApiPhoto(ApiBase): return Photo(self._client, result) def upload_encoded(self, photo_file, **kwds): - """ Base64-encodes and uploads the specified file """ + """ + Endpoint: /photo/upload.json + + Base64-encodes and uploads the specified photo filename. + """ with open(photo_file, "rb") as in_file: encoded_photo = base64.b64encode(in_file.read()) result = self._client.post("/photo/upload.json", photo=encoded_photo, @@ -119,8 +153,11 @@ class ApiPhoto(ApiBase): """ Not yet implemented """ raise NotImplementedError() + # TODO: Add options def next_previous(self, photo, **kwds): """ + Endpoint: /photo//nextprevious.json + Returns a dict containing the next and previous photo lists (there may be more than one next/previous photo returned). """ @@ -130,8 +167,11 @@ class ApiPhoto(ApiBase): def transform(self, photo, **kwds): """ - Performs transformation specified in **kwds - Example: transform(photo, rotate=90) + Endpoint: /photo//transform.json + + Performs the specified transformations. + eg. transform(photo, rotate=90) + Returns the transformed photo. """ if not isinstance(photo, Photo): photo = Photo(self._client, {"id": photo}) diff --git a/trovebox/objects/album.py b/trovebox/objects/album.py index b2acdb4..b09b471 100644 --- a/trovebox/objects/album.py +++ b/trovebox/objects/album.py @@ -18,6 +18,8 @@ class Album(TroveboxObject): if isinstance(self.cover, dict): self.cover = Photo(self._trovebox, self.cover) + # def cover_update(self, photo, **kwds): + def delete(self, **kwds): """ Endpoint: /album//delete.json @@ -37,10 +39,12 @@ class Album(TroveboxObject): """ Not implemented yet """ raise NotImplementedError() + # TODO: Should be just "add" def add_photos(self, photos, **kwds): """ Not implemented yet """ raise NotImplementedError() + # TODO: Should be just "remove" def remove_photos(self, photos, **kwds): """ Not implemented yet """ raise NotImplementedError() diff --git a/trovebox/objects/photo.py b/trovebox/objects/photo.py index ffb66af..47a9901 100644 --- a/trovebox/objects/photo.py +++ b/trovebox/objects/photo.py @@ -8,7 +8,9 @@ class Photo(TroveboxObject): """ Representation of a Photo object """ def delete(self, **kwds): """ - Delete this photo. + Endpoint: /photo//delete.json + + Deletes this photo. Returns True if successful. Raises a TroveboxError if not. """ @@ -19,8 +21,14 @@ class Photo(TroveboxObject): self._delete_fields() return result + # def delete_source(self, **kwds): + def edit(self, **kwds): - """ Returns an HTML form to edit the photo """ + """ + Endpoint: /photo//edit.json + + Returns an HTML form to edit this photo's attributes. + """ result = self._trovebox.get("/photo/%s/edit.json" % self.id, **kwds)["result"] return result["markup"] @@ -34,14 +42,23 @@ class Photo(TroveboxObject): raise NotImplementedError() def update(self, **kwds): - """ Update this photo with the specified parameters """ + """ + Endpoint: /photo//update.json + + Updates this photo with the specified parameters. + """ result = self._trovebox.post("/photo/%s/update.json" % self.id, **kwds)["result"] self._replace_fields(result) + # TODO: Add options def view(self, **kwds): """ - Used to view the photo at a particular size. + Endpoint: /photo//view.json + + Requests all properties of this photo. + Can be used to obtain URLs for the photo at a particular size, + by using the "returnSizes" parameter. Updates the photo's fields with the response. """ result = self._trovebox.get("/photo/%s/view.json" % @@ -52,8 +69,11 @@ class Photo(TroveboxObject): """ Not implemented yet """ raise NotImplementedError() + # TODO: Add options def next_previous(self, **kwds): """ + Endpoint: /photo//nextprevious.json + Returns a dict containing the next and previous photo lists (there may be more than one next/previous photo returned). """ @@ -82,8 +102,11 @@ class Photo(TroveboxObject): def transform(self, **kwds): """ - Performs transformation specified in **kwds - Example: transform(rotate=90) + Endpoint: /photo//transform.json + + Performs the specified transformations. + eg. transform(photo, rotate=90) + Updates the photo's fields with the response. """ result = self._trovebox.post("/photo/%s/transform.json" % self.id, **kwds)["result"] From adecf7aad0df262450a2f4d17d10798aaffc2d8f Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 7 Sep 2013 11:19:15 +0100 Subject: [PATCH 36/80] Updated docstrings --- trovebox/api/api_tag.py | 26 +++++++++++++++++++++----- trovebox/objects/tag.py | 13 +++++++++++-- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/trovebox/api/api_tag.py b/trovebox/api/api_tag.py index dd11b15..fbab095 100644 --- a/trovebox/api/api_tag.py +++ b/trovebox/api/api_tag.py @@ -8,7 +8,11 @@ from .api_base import ApiBase class ApiTags(ApiBase): """ Definitions of /tags/ API endpoints """ def list(self, **kwds): - """ Returns a list of Tag objects """ + """ + Endpoint: /tags/list.json + + Returns a list of Tag objects. + """ tags = self._client.get("/tags/list.json", **kwds)["result"] tags = http.result_to_list(tags) return [Tag(self._client, tag) for tag in tags] @@ -17,14 +21,19 @@ class ApiTag(ApiBase): """ Definitions of /tag/ API endpoints """ def create(self, tag, **kwds): """ - Create a new tag. - The API returns true if the tag was sucessfully created + Endpoint: /tag/create.json + + Creates a new tag. + Returns True if successful. + Raises a TroveboxError if not. """ return self._client.post("/tag/create.json", tag=tag, **kwds)["result"] def delete(self, tag, **kwds): """ - Delete a tag. + Endpoint: /tag//delete.json + + Deletes a tag. Returns True if successful. Raises a TroveboxError if not. """ @@ -33,8 +42,15 @@ class ApiTag(ApiBase): return tag.delete(**kwds) def update(self, tag, **kwds): - """ Update a tag """ + """ + Endpoint: /tag//update.json + + Updates a tag with the specified parameters. + Returns the updated tag object. + """ if not isinstance(tag, Tag): tag = Tag(self._client, {"id": tag}) tag.update(**kwds) return tag + + # def view(self, tag, **kwds): diff --git a/trovebox/objects/tag.py b/trovebox/objects/tag.py index 94d5ac4..5c9b905 100644 --- a/trovebox/objects/tag.py +++ b/trovebox/objects/tag.py @@ -13,7 +13,9 @@ class Tag(TroveboxObject): """ Representation of a Tag object """ def delete(self, **kwds): """ - Delete this tag. + Endpoint: /tag//delete.json + + Deletes this tag. Returns True if successful. Raises a TroveboxError if not. """ @@ -25,7 +27,14 @@ class Tag(TroveboxObject): return result def update(self, **kwds): - """ Update this tag with the specified parameters """ + """ + Endpoint: /tag//update.json + + Updates this tag with the specified parameters. + Returns the updated tag object. + """ result = self._trovebox.post("/tag/%s/update.json" % quote(self.id), **kwds)["result"] self._replace_fields(result) + + # def view(self, **kwds): From a4add97f5924bcd0aaaeef5b3b705885c68f2cae Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 7 Sep 2013 11:30:27 +0100 Subject: [PATCH 37/80] Remove all endpoints that return forms. --- tests/functional/test_albums.py | 5 ----- tests/functional/test_photos.py | 10 ---------- tests/unit/test_albums.py | 19 ------------------- tests/unit/test_photos.py | 28 ---------------------------- trovebox/api/api_album.py | 4 ---- trovebox/api/api_photo.py | 10 ---------- trovebox/objects/album.py | 4 ---- trovebox/objects/photo.py | 10 ---------- 8 files changed, 90 deletions(-) diff --git a/tests/functional/test_albums.py b/tests/functional/test_albums.py index 2b1b21f..276225a 100644 --- a/tests/functional/test_albums.py +++ b/tests/functional/test_albums.py @@ -63,11 +63,6 @@ class TestAlbums(test_base.TestBase): for photo in self.photos: self.assertIn(photo.id, [p.id for p in album.photos]) - def test_form(self): - """ If album.form gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.client.album.form(None) - def test_add_photos(self): """ If album.add_photos gets implemented, write a test! """ with self.assertRaises(NotImplementedError): diff --git a/tests/functional/test_photos.py b/tests/functional/test_photos.py index 6a7577e..19f4bb5 100644 --- a/tests/functional/test_photos.py +++ b/tests/functional/test_photos.py @@ -47,16 +47,6 @@ class TestPhotos(test_base.TestBase): self._delete_all() self._create_test_photos() - def test_edit(self): - """ Check that the edit request returns an HTML form """ - # Test using the Trovebox class - html = self.client.photo.edit(self.photos[0]) - self.assertIn(""}) - result = self.client.photo.edit(self.test_photos[0]) - mock_get.assert_called_with("/photo/1a/edit.json") - self.assertEqual(result, "
") - - @mock.patch.object(trovebox.Trovebox, 'get') - def test_photo_edit_id(self, mock_get): - """Check that a the photo edit endpoint is working when using an ID""" - mock_get.return_value = self._return_value({"markup": ""}) - result = self.client.photo.edit("1a") - mock_get.assert_called_with("/photo/1a/edit.json") - self.assertEqual(result, "") - - @mock.patch.object(trovebox.Trovebox, 'get') - def test_photo_object_edit(self, mock_get): - """ - Check that a the photo edit endpoint is working - when using the photo object directly - """ - mock_get.return_value = self._return_value({"markup": ""}) - result = self.test_photos[0].edit() - mock_get.assert_called_with("/photo/1a/edit.json") - self.assertEqual(result, "") - class TestPhotoReplace(TestPhotos): @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_replace(self, _): diff --git a/trovebox/api/api_album.py b/trovebox/api/api_album.py index eaa147b..fca8cc7 100644 --- a/trovebox/api/api_album.py +++ b/trovebox/api/api_album.py @@ -43,10 +43,6 @@ class ApiAlbum(ApiBase): album = Album(self._client, {"id": album}) return album.delete(**kwds) - def form(self, album, **kwds): - """ Not yet implemented """ - raise NotImplementedError() - # TODO: Should be just "add" def add_photos(self, album, photos, **kwds): """ Not yet implemented """ diff --git a/trovebox/api/api_photo.py b/trovebox/api/api_photo.py index 885ac61..eca7f0c 100644 --- a/trovebox/api/api_photo.py +++ b/trovebox/api/api_photo.py @@ -80,16 +80,6 @@ class ApiPhoto(ApiBase): # def delete_source(self, photo, **kwds): - def edit(self, photo, **kwds): - """ - Endpoint: /photo//edit.json - - Returns an HTML form to edit a photo's attributes. - """ - if not isinstance(photo, Photo): - photo = Photo(self._client, {"id": photo}) - return photo.edit(**kwds) - def replace(self, photo, photo_file, **kwds): """ Not yet implemented """ raise NotImplementedError() diff --git a/trovebox/objects/album.py b/trovebox/objects/album.py index b09b471..6813797 100644 --- a/trovebox/objects/album.py +++ b/trovebox/objects/album.py @@ -35,10 +35,6 @@ class Album(TroveboxObject): self._delete_fields() return result - def form(self, **kwds): - """ Not implemented yet """ - raise NotImplementedError() - # TODO: Should be just "add" def add_photos(self, photos, **kwds): """ Not implemented yet """ diff --git a/trovebox/objects/photo.py b/trovebox/objects/photo.py index 47a9901..03e7435 100644 --- a/trovebox/objects/photo.py +++ b/trovebox/objects/photo.py @@ -23,16 +23,6 @@ class Photo(TroveboxObject): # def delete_source(self, **kwds): - def edit(self, **kwds): - """ - Endpoint: /photo//edit.json - - Returns an HTML form to edit this photo's attributes. - """ - result = self._trovebox.get("/photo/%s/edit.json" % - self.id, **kwds)["result"] - return result["markup"] - def replace(self, photo_file, **kwds): """ Not implemented yet """ raise NotImplementedError() From ba9ef81ac8e01099a72dd1c8bbd2811c7f12616a Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 7 Sep 2013 11:44:03 +0100 Subject: [PATCH 38/80] Put the album.photos attribute back, since this is used for album.view when includeElements=True. --- tests/unit/test_albums.py | 21 ++++++++++++++------- trovebox/objects/album.py | 6 ++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_albums.py b/tests/unit/test_albums.py index 513500c..4908660 100644 --- a/tests/unit/test_albums.py +++ b/tests/unit/test_albums.py @@ -9,16 +9,21 @@ import trovebox class TestAlbums(unittest.TestCase): test_host = "test.example.com" + test_photo_dict = {"id": "1a", "tags": ["tag1", "tag2"]} test_albums_dict = [{"cover": {"id": "1a", "tags": ["tag1", "tag2"]}, "id": "1", "name": "Album 1", + "photos": [test_photo_dict], "totalRows": 2}, {"cover": {"id": "2b", "tags": ["tag3", "tag4"]}, "id": "2", "name": "Album 2", + "photos": [test_photo_dict], "totalRows": 2}] def setUp(self): self.client = trovebox.Trovebox(host=self.test_host) + self.test_photo = trovebox.objects.photo.Photo(self.client, + self.test_photo_dict) self.test_albums = [trovebox.objects.album.Album(self.client, album) for album in self.test_albums_dict] @@ -228,33 +233,35 @@ class TestAlbumView(TestAlbums): def test_album_view(self, mock_get): """Check that an album can be viewed""" mock_get.return_value = self._return_value(self.test_albums_dict[1]) - result = self.client.album.view(self.test_albums[0], name="Test") - mock_get.assert_called_with("/album/1/view.json", name="Test") + result = self.client.album.view(self.test_albums[0], includeElements=True) + mock_get.assert_called_with("/album/1/view.json", includeElements=True) self.assertEqual(result.id, "2") self.assertEqual(result.name, "Album 2") self.assertEqual(result.cover.id, "2b") self.assertEqual(result.cover.tags, ["tag3", "tag4"]) + self.assertEqual(result.photos[0].id, self.test_photo.id) @mock.patch.object(trovebox.Trovebox, 'get') def test_album_view_id(self, mock_get): """Check that an album can be viewed using its ID""" mock_get.return_value = self._return_value(self.test_albums_dict[1]) - result = self.client.album.view("1", name="Test") - mock_get.assert_called_with("/album/1/view.json", name="Test") + result = self.client.album.view("1", includeElements=True) + mock_get.assert_called_with("/album/1/view.json", includeElements=True) self.assertEqual(result.id, "2") self.assertEqual(result.name, "Album 2") self.assertEqual(result.cover.id, "2b") self.assertEqual(result.cover.tags, ["tag3", "tag4"]) + self.assertEqual(result.photos[0].id, self.test_photo.id) @mock.patch.object(trovebox.Trovebox, 'get') def test_album_object_view(self, mock_get): """Check that an album can be viewed using the album object directly""" mock_get.return_value = self._return_value(self.test_albums_dict[1]) album = self.test_albums[0] - album.view(name="Test") - mock_get.assert_called_with("/album/1/view.json", name="Test") + album.view(includeElements=True) + mock_get.assert_called_with("/album/1/view.json", includeElements=True) self.assertEqual(album.id, "2") self.assertEqual(album.name, "Album 2") self.assertEqual(album.cover.id, "2b") self.assertEqual(album.cover.tags, ["tag3", "tag4"]) - + self.assertEqual(album.photos[0].id, self.test_photo.id) diff --git a/trovebox/objects/album.py b/trovebox/objects/album.py index 62b898c..21d6958 100644 --- a/trovebox/objects/album.py +++ b/trovebox/objects/album.py @@ -8,6 +8,7 @@ from .photo import Photo class Album(TroveboxObject): """ Representation of an Album object """ def __init__(self, trovebox, json_dict): + self.photos = None self.cover = None TroveboxObject.__init__(self, trovebox, json_dict) self._update_fields_with_objects() @@ -17,6 +18,11 @@ class Album(TroveboxObject): # Update the cover with a photo object if isinstance(self.cover, dict): self.cover = Photo(self._trovebox, self.cover) + # Update the photo list with photo objects + if isinstance(self.photos, list): + for i, photo in enumerate(self.photos): + if isinstance(photo, dict): + self.photos[i] = Photo(self._trovebox, photo) def delete(self, **kwds): """ From 3eb2cba1803c66a4cd93a2739df08538c211e897 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 7 Sep 2013 11:58:36 +0100 Subject: [PATCH 39/80] Skip all activity tests in V1, since activities never get deleted in this version. This makes it too hard to test. --- tests/functional/test_activities.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_activities.py b/tests/functional/test_activities.py index 4f7bbf7..c07cc33 100644 --- a/tests/functional/test_activities.py +++ b/tests/functional/test_activities.py @@ -5,11 +5,12 @@ except ImportError: from tests.functional import test_base +@unittest.skipIf(test_base.get_test_server_api() == 1, + ("Activities never get deleted in v1, which makes " + "these tests too hard to write")) class TestActivities(test_base.TestBase): testcase_name = "activity API" - @unittest.skipIf(test_base.get_test_server_api() == 1, - "The activity/list endpoint behaves differenty at v1") def test_list(self): """ Upload three photos, and check that three corresponding activities From f2a9312b0f53f48007129a23f6efe7f0715b903f Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 7 Sep 2013 13:59:55 +0100 Subject: [PATCH 40/80] Use pylint-patcher rather than my forked version of Pylint --- .travis.yml | 8 +++++--- .travis/install_pylint | 10 ---------- 2 files changed, 5 insertions(+), 13 deletions(-) delete mode 100755 .travis/install_pylint diff --git a/.travis.yml b/.travis.yml index 0114cb0..80c2f98 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ install: # Install test dependencies - pip install tox --use-mirrors - pip install coveralls --use-mirrors - - .travis/install_pylint script: tox @@ -14,8 +13,11 @@ after_success: after_script: # Install dependencies for Pylint - - pip install requests requests-oauthlib --use-mirrors + - pip install pylint-patcher --use-mirrors + - pip install requests --use-mirrors + - pip install requests-oauthlib --use-mirrors # Run Pylint + # Uses pylint-patcher to allow exceptions to be stored in a patchfile # (for information only, any errors don't affect the Travis result) - - pylint --use-ignore-patch=y trovebox + - pylint-patcher trovebox diff --git a/.travis/install_pylint b/.travis/install_pylint deleted file mode 100755 index 0f5ce45..0000000 --- a/.travis/install_pylint +++ /dev/null @@ -1,10 +0,0 @@ -# Until the --use-ignore-patch makes it into pylint upstream, we need to -# download and install from sneakypete81's pylint fork - -wget https://bitbucket.org/sneakypete81/pylint/get/use_ignore_patch.zip -unzip use_ignore_patch.zip -cd sneakypete81-pylint-* -python setup.py install - -cd .. -rm -r sneakypete81-pylint-* From c0a76721dff30b0a53b8c04345778379b20e32ed Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 7 Sep 2013 22:09:51 +0100 Subject: [PATCH 41/80] Update Pylint ignores to remove timestamps from patchfile --- trovebox/.pylint-ignores.patch | 108 +++++++++++++++------------------ 1 file changed, 48 insertions(+), 60 deletions(-) diff --git a/trovebox/.pylint-ignores.patch b/trovebox/.pylint-ignores.patch index a4d8f5d..1f4c970 100644 --- a/trovebox/.pylint-ignores.patch +++ b/trovebox/.pylint-ignores.patch @@ -1,60 +1,60 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_activity.py patched/api/api_activity.py ---- original/api/api_activity.py 2013-08-19 17:59:15.592149000 +0100 -+++ patched/api/api_activity.py 2013-08-19 18:08:39.950947589 +0100 +--- original/api/api_activity.py ++++ patched/api/api_activity.py @@ -22,7 +22,7 @@ raise TroveboxError("Purge response returned False") return True -class ApiActivity(object): -+class ApiActivity(object): # pylint: disable=R0903 ++class ApiActivity(object): # pylint: disable=too-few-public-methods """ Definitions of /activity/ API endpoints """ def __init__(self, client): self._client = client diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_album.py patched/api/api_album.py ---- original/api/api_album.py 2013-08-19 16:09:53.539609000 +0100 -+++ patched/api/api_album.py 2013-08-19 18:08:20.118849270 +0100 -@@ -3,7 +3,7 @@ - """ +--- original/api/api_album.py ++++ patched/api/api_album.py +@@ -4,7 +4,7 @@ from trovebox.objects.album import Album + from trovebox import http -class ApiAlbums(object): -+class ApiAlbums(object): # pylint: disable=R0903 ++class ApiAlbums(object): # pylint: disable=too-few-public-methods """ Definitions of /albums/ API endpoints """ def __init__(self, client): self._client = client diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_tag.py patched/api/api_tag.py ---- original/api/api_tag.py 2013-08-19 16:09:53.539609000 +0100 -+++ patched/api/api_tag.py 2013-08-19 18:08:20.118849270 +0100 -@@ -3,7 +3,7 @@ - """ +--- original/api/api_tag.py ++++ patched/api/api_tag.py +@@ -4,7 +4,7 @@ + from trovebox import http from trovebox.objects.tag import Tag -class ApiTags(object): -+class ApiTags(object): # pylint: disable=R0903 ++class ApiTags(object): # pylint: disable=too-few-public-methods """ Definitions of /tags/ API endpoints """ def __init__(self, client): self._client = client diff --unified --recursive '--exclude=.pylint-ignores.patch' original/auth.py patched/auth.py ---- original/auth.py 2013-08-19 16:09:53.543609000 +0100 -+++ patched/auth.py 2013-08-19 18:08:20.118849270 +0100 +--- original/auth.py ++++ patched/auth.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import os try: - from configparser import ConfigParser # Python3 -+ from configparser import ConfigParser # Python3 # pylint: disable=F0401 ++ from configparser import ConfigParser # Python3 # pylint: disable=import-error except ImportError: from ConfigParser import SafeConfigParser as ConfigParser # Python2 try: @@ -12,9 +12,9 @@ - except ImportError: + except ImportError: # pragma: no cover import StringIO as io # Python2 -class Auth(object): -+class Auth(object): # pylint: disable=R0903 ++class Auth(object): # pylint: disable=too-few-public-methods """OAuth secrets""" - def __init__(self, config_file, host, -+ def __init__(self, config_file, host, # pylint: disable=R0913 ++ def __init__(self, config_file, host, # pylint: disable=too-many-arguments consumer_key, consumer_secret, token, token_secret): if host is None: @@ -63,33 +63,33 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/auth.py pa parser.optionxform = str # Case-sensitive options try: - parser.read_file(buf) # Python3 -+ parser.read_file(buf) # Python3 # pylint: disable=E1103 ++ parser.read_file(buf) # Python3 # pylint: disable=maybe-no-member except AttributeError: parser.readfp(buf) # Python2 diff --unified --recursive '--exclude=.pylint-ignores.patch' original/http.py patched/http.py ---- original/http.py 2013-08-19 16:09:53.543609000 +0100 -+++ patched/http.py 2013-08-19 18:08:20.118849270 +0100 +--- original/http.py ++++ patched/http.py @@ -7,18 +7,18 @@ import requests_oauthlib import logging try: - from urllib.parse import urlparse, urlunparse # Python3 -+ from urllib.parse import urlparse, urlunparse # Python3 # pylint: disable=F0401,E0611 ++ from urllib.parse import urlparse, urlunparse # Python3 # pylint: disable=import-error,no-name-in-module except ImportError: from urlparse import urlparse, urlunparse # Python2 from trovebox.objects.trovebox_object import TroveboxObject -from .errors import * -+from .errors import * # pylint: disable=W0401 ++from .errors import * # pylint: disable=wildcard-import from .auth import Auth if sys.version < '3': - TEXT_TYPE = unicode -+ TEXT_TYPE = unicode # pylint: disable=C0103 - else: ++ TEXT_TYPE = unicode # pylint: disable=invalid-name + else: # pragma: no cover - TEXT_TYPE = str -+ TEXT_TYPE = str # pylint: disable=C0103 ++ TEXT_TYPE = str # pylint: disable=invalid-name DUPLICATE_RESPONSE = {"code": 409, "message": "This photo already exists"} @@ -98,19 +98,19 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/http.py pa } - def __init__(self, config_file=None, host=None, -+ def __init__(self, config_file=None, host=None, # pylint: disable=R0913 ++ def __init__(self, config_file=None, host=None, # pylint: disable=too-many-arguments consumer_key='', consumer_secret='', token='', token_secret='', api_version=None): diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.py patched/__init__.py ---- original/__init__.py 2013-08-19 17:02:22.951226000 +0100 -+++ patched/__init__.py 2013-08-19 18:08:36.194928993 +0100 +--- original/__init__.py ++++ patched/__init__.py @@ -2,7 +2,7 @@ __init__.py : Trovebox package top level """ from .http import Http -from .errors import * -+from .errors import * # pylint: disable=W0401 ++from .errors import * # pylint: disable=wildcard-import from ._version import __version__ from trovebox.api import api_photo from trovebox.api import api_tag @@ -119,7 +119,7 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.p LATEST_API_VERSION = 2 -class Trovebox(Http): -+class Trovebox(Http): # pylint: disable=R0902 ++class Trovebox(Http): # pylint: disable=too-many-instance-attributes """ Client library for Trovebox If no parameters are specified, config is loaded from the default @@ -128,73 +128,61 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.p even if the Trovebox API is updated to a new revision. """ - def __init__(self, config_file=None, host=None, -+ def __init__(self, config_file=None, host=None, # pylint: disable=R0913 ++ def __init__(self, config_file=None, host=None, # pylint: disable=too-many-arguments consumer_key='', consumer_secret='', token='', token_secret='', api_version=None): diff --unified --recursive '--exclude=.pylint-ignores.patch' original/main.py patched/main.py ---- original/main.py 2013-08-19 16:09:53.543609000 +0100 -+++ patched/main.py 2013-08-19 18:08:20.118849270 +0100 +--- original/main.py ++++ patched/main.py @@ -26,7 +26,7 @@ ################################################################# -def main(args=sys.argv[1:]): -+def main(args=sys.argv[1:]): # pylint: disable=R0912,C0111 ++def main(args=sys.argv[1:]): # pylint: disable=too-many-branches usage = "%prog --help" parser = OptionParser(usage, add_help_option=False) parser.add_option('-c', '--config', help="Configuration file to use", -@@ -84,13 +84,13 @@ +@@ -84,11 +84,11 @@ sys.exit(1) if options.method == "GET": - result = client.get(options.endpoint, process_response=False, -+ result = client.get(options.endpoint, process_response=False, # pylint: disable=W0142 ++ result = client.get(options.endpoint, process_response=False, # pylint: disable=star-args **params) else: params, files = extract_files(params) - result = client.post(options.endpoint, process_response=False, -+ result = client.post(options.endpoint, process_response=False, # pylint: disable=W0142 ++ result = client.post(options.endpoint, process_response=False, # pylint: disable=star-args files=files, **params) -- for f in files: -+ for f in files: # pylint: disable=C0103 + for f in files: files[f].close() - - if options.verbose: diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/tag.py patched/objects/tag.py ---- original/objects/tag.py 2013-08-19 16:09:53.543609000 +0100 -+++ patched/objects/tag.py 2013-08-19 18:08:20.118849270 +0100 -@@ -1,8 +1,8 @@ --""" -+""" # pylint: disable=R0801 +--- original/objects/tag.py ++++ patched/objects/tag.py +@@ -2,7 +2,7 @@ Representation of a Tag object """ try: - from urllib.parse import quote # Python3 -+ from urllib.parse import quote # Python3 # pylint: disable=F0401,E0611 ++ from urllib.parse import quote # Python3 # pylint: disable=import-error,no-name-in-module except ImportError: from urllib import quote # Python2 diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/trovebox_object.py patched/objects/trovebox_object.py ---- original/objects/trovebox_object.py 2013-08-19 16:09:53.543609000 +0100 -+++ patched/objects/trovebox_object.py 2013-08-19 18:08:20.118849270 +0100 +--- original/objects/trovebox_object.py ++++ patched/objects/trovebox_object.py @@ -1,10 +1,10 @@ """ Base object supporting the storage of custom fields as attributes """ -class TroveboxObject(object): -+class TroveboxObject(object): # pylint: disable=R0903 ++class TroveboxObject(object): # pylint: disable=too-few-public-methods """ Base object supporting the storage of custom fields as attributes """ def __init__(self, trovebox, json_dict): - self.id = None -+ self.id = None # pylint: disable=C0103 ++ self.id = None # pylint: disable=invalid-name self.name = None self._trovebox = trovebox self._json_dict = json_dict -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/_version.py patched/_version.py ---- original/_version.py 2013-08-19 16:09:53.543609000 +0100 -+++ patched/_version.py 2013-08-19 18:08:20.118849270 +0100 -@@ -1,2 +1,2 @@ -- -+ # pylint: disable=C0111 - __version__ = "0.5" From 3bd733b2292882c053f7e0863f3476cd83365744 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 8 Sep 2013 12:07:27 +0100 Subject: [PATCH 42/80] Added album cover update endpoint --- tests/functional/test_albums.py | 6 +++++ tests/unit/test_albums.py | 39 +++++++++++++++++++++++++++++++++ trovebox/api/api_album.py | 12 +++++++++- trovebox/objects/album.py | 21 +++++++++++++++++- 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_albums.py b/tests/functional/test_albums.py index 276225a..3bba1ef 100644 --- a/tests/functional/test_albums.py +++ b/tests/functional/test_albums.py @@ -53,6 +53,12 @@ class TestAlbums(test_base.TestBase): self.albums = self.client.albums.list() self.assertEqual(self.albums[0].name, self.TEST_ALBUM) + def test_update_cover(self): + """ Test that an album cover can be updated """ + self.assertNotEqual(self.albums[0].cover.id, self.photos[1].id) + self.albums[0].cover_update(self.photos[1]) + self.assertEqual(self.albums[0].cover.id, self.photos[1].id) + def test_view(self): """ Test the album view """ album = self.albums[0] diff --git a/tests/unit/test_albums.py b/tests/unit/test_albums.py index 180d0d1..23c2955 100644 --- a/tests/unit/test_albums.py +++ b/tests/unit/test_albums.py @@ -76,6 +76,45 @@ class TestAlbumsList(TestAlbums): self.assertEqual(result[1].cover.id, "2b") self.assertEqual(result[1].cover.tags, ["tag3", "tag4"]) +class TestAlbumUpdateCover(TestAlbums): + @mock.patch.object(trovebox.Trovebox, 'post') + def test_album_cover_update(self, mock_post): + """Check that an album cover can be updated""" + mock_post.return_value = self._return_value(self.test_albums_dict[1]) + result = self.client.album.cover_update(self.test_albums[0], + self.test_photo, foo="bar") + mock_post.assert_called_with("/album/1/cover/1a/update.json", + foo="bar") + self.assertEqual(result.id, "2") + self.assertEqual(result.name, "Album 2") + self.assertEqual(result.cover.id, "2b") + self.assertEqual(result.cover.tags, ["tag3", "tag4"]) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_album_cover_update_id(self, mock_post): + """Check that an album cover can be updated using IDs""" + mock_post.return_value = self._return_value(self.test_albums_dict[1]) + result = self.client.album.cover_update("1", "1a", foo="bar") + mock_post.assert_called_with("/album/1/cover/1a/update.json", + foo="bar") + self.assertEqual(result.id, "2") + self.assertEqual(result.name, "Album 2") + self.assertEqual(result.cover.id, "2b") + self.assertEqual(result.cover.tags, ["tag3", "tag4"]) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_album_object_cover_update(self, mock_post): + """Check that an album cover can be updated using the album object directly""" + mock_post.return_value = self._return_value(self.test_albums_dict[1]) + album = self.test_albums[0] + album.cover_update(self.test_photo, foo="bar") + mock_post.assert_called_with("/album/1/cover/1a/update.json", + foo="bar") + self.assertEqual(album.id, "2") + self.assertEqual(album.name, "Album 2") + self.assertEqual(album.cover.id, "2b") + self.assertEqual(album.cover.tags, ["tag3", "tag4"]) + class TestAlbumCreate(TestAlbums): @mock.patch.object(trovebox.Trovebox, 'post') def test_album_create(self, mock_post): diff --git a/trovebox/api/api_album.py b/trovebox/api/api_album.py index fca8cc7..c3e6d29 100644 --- a/trovebox/api/api_album.py +++ b/trovebox/api/api_album.py @@ -19,7 +19,17 @@ class ApiAlbums(ApiBase): class ApiAlbum(ApiBase): """ Definitions of /album/ API endpoints """ - # def cover_update(self, album, photo, **kwds): + def cover_update(self, album, photo, **kwds): + """ + Endpoint: /album//cover//update.json + + Update the cover photo of an album. + Returns the updated album object. + """ + if not isinstance(album, Album): + album = Album(self._client, {"id": album}) + album.cover_update(photo, **kwds) + return album def create(self, name, **kwds): """ diff --git a/trovebox/objects/album.py b/trovebox/objects/album.py index 65f4d4e..570aa37 100644 --- a/trovebox/objects/album.py +++ b/trovebox/objects/album.py @@ -24,7 +24,26 @@ class Album(TroveboxObject): if isinstance(photo, dict): self.photos[i] = Photo(self._trovebox, photo) - # def cover_update(self, photo, **kwds): + def cover_update(self, photo, **kwds): + """ + Endpoint: /album//cover//update.json + + Update the cover photo of this album. + """ + if not isinstance(photo, Photo): + photo = Photo(self._trovebox, {"id": photo}) + + result = self._trovebox.post("/album/%s/cover/%s/update.json" % + (self.id, photo.id), + **kwds)["result"] + + # API currently doesn't return the updated album + # (frontend issue #1369) + if isinstance(result, bool): # pragma: no cover + result = self._trovebox.get("/album/%s/view.json" % + self.id)["result"] + self._replace_fields(result) + self._update_fields_with_objects() def delete(self, **kwds): """ From f8aecde457786e6c5262a2f5b0460a078a5f472c Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 8 Sep 2013 17:22:25 +0100 Subject: [PATCH 43/80] Added album add/remove endpoints --- tests/functional/test_albums.py | 35 +++++-- tests/unit/test_actions.py | 9 +- tests/unit/test_albums.py | 152 +++++++++++++++++++++------- trovebox/api/api_action.py | 9 +- trovebox/api/api_activity.py | 4 +- trovebox/api/api_album.py | 63 ++++++++++-- trovebox/api/api_base.py | 6 +- trovebox/objects/action.py | 1 + trovebox/objects/activity.py | 1 + trovebox/objects/album.py | 59 +++++++++-- trovebox/objects/photo.py | 4 + trovebox/objects/tag.py | 4 + trovebox/objects/trovebox_object.py | 5 + 13 files changed, 273 insertions(+), 79 deletions(-) diff --git a/tests/functional/test_albums.py b/tests/functional/test_albums.py index 3bba1ef..d6b3949 100644 --- a/tests/functional/test_albums.py +++ b/tests/functional/test_albums.py @@ -1,4 +1,5 @@ from tests.functional import test_base +from trovebox.objects.album import Album class TestAlbums(test_base.TestBase): testcase_name = "album API" @@ -61,20 +62,34 @@ class TestAlbums(test_base.TestBase): def test_view(self): """ Test the album view """ - album = self.albums[0] + # Do a view() with includeElements=False, using a fresh Album object + album = Album(self.client, {"id": self.albums[0].id}) + album.view() + # Make sure there are no photos reported + self.assertEqual(album.photos, None) - # Get the photos in the album using the Album object directly + # Get the photos with includeElements=True album.view(includeElements=True) # Make sure all photos are in the album for photo in self.photos: self.assertIn(photo.id, [p.id for p in album.photos]) - def test_add_photos(self): - """ If album.add_photos gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.client.album.add_photos(None, None) + def test_add_remove(self): + """ Test that photos can be added and removed from an album """ + # Make sure all photos are in the album + album = self.albums[0] + album.view(includeElements=True) + for photo in self.photos: + self.assertIn(photo.id, [p.id for p in album.photos]) - def test_remove_photos(self): - """ If album.remove_photos gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.client.album.remove_photos(None, None) + # Remove two photos and check that they're gone + album.remove(self.photos[:2]) + album.view(includeElements=True) + self.assertEqual([p.id for p in album.photos], [self.photos[2].id]) + + # Add a photo and check that it's there + album.add(self.photos[1]) + album.view(includeElements=True) + self.assertNotIn(self.photos[0].id, [p.id for p in album.photos]) + self.assertIn(self.photos[1].id, [p.id for p in album.photos]) + self.assertIn(self.photos[2].id, [p.id for p in album.photos]) diff --git a/tests/unit/test_actions.py b/tests/unit/test_actions.py index 6a14006..cbbb6b2 100644 --- a/tests/unit/test_actions.py +++ b/tests/unit/test_actions.py @@ -66,13 +66,16 @@ class TestActionCreate(TestActions): @mock.patch.object(trovebox.Trovebox, 'post') def test_action_create_invalid_type(self, mock_post): - """Check that an exception is raised if an action is created on a non photo object""" - with self.assertRaises(NotImplementedError): + """ + Check that an exception is raised if an action is created on an + invalid object. + """ + with self.assertRaises(AttributeError): self.client.action.create(target=object(), foo="bar") @mock.patch.object(trovebox.Trovebox, 'post') def test_action_create_invalid_return_type(self, mock_post): - """Check that an exception is raised if an non photo object is returned""" + """Check that an exception is raised if an invalid object is returned""" mock_post.return_value = self._return_value({"target": "test", "target_type": "invalid"}) with self.assertRaises(NotImplementedError): diff --git a/tests/unit/test_albums.py b/tests/unit/test_albums.py index 23c2955..3647f3c 100644 --- a/tests/unit/test_albums.py +++ b/tests/unit/test_albums.py @@ -9,21 +9,22 @@ import trovebox class TestAlbums(unittest.TestCase): test_host = "test.example.com" - test_photo_dict = {"id": "1a", "tags": ["tag1", "tag2"]} + test_photos_dict = [{"id": "1a", "tags": ["tag1", "tag2"]}, + {"id": "2b", "tags": ["tag3", "tag4"]}] test_albums_dict = [{"cover": {"id": "1a", "tags": ["tag1", "tag2"]}, "id": "1", "name": "Album 1", - "photos": [test_photo_dict], + "photos": [test_photos_dict[0]], "totalRows": 2}, {"cover": {"id": "2b", "tags": ["tag3", "tag4"]}, "id": "2", "name": "Album 2", - "photos": [test_photo_dict], + "photos": [test_photos_dict[1]], "totalRows": 2}] def setUp(self): self.client = trovebox.Trovebox(host=self.test_host) - self.test_photo = trovebox.objects.photo.Photo(self.client, - self.test_photo_dict) + self.test_photos = [trovebox.objects.photo.Photo(self.client, photo) + for photo in self.test_photos_dict] self.test_albums = [trovebox.objects.album.Album(self.client, album) for album in self.test_albums_dict] @@ -82,7 +83,7 @@ class TestAlbumUpdateCover(TestAlbums): """Check that an album cover can be updated""" mock_post.return_value = self._return_value(self.test_albums_dict[1]) result = self.client.album.cover_update(self.test_albums[0], - self.test_photo, foo="bar") + self.test_photos[0], foo="bar") mock_post.assert_called_with("/album/1/cover/1a/update.json", foo="bar") self.assertEqual(result.id, "2") @@ -107,8 +108,8 @@ class TestAlbumUpdateCover(TestAlbums): """Check that an album cover can be updated using the album object directly""" mock_post.return_value = self._return_value(self.test_albums_dict[1]) album = self.test_albums[0] - album.cover_update(self.test_photo, foo="bar") - mock_post.assert_called_with("/album/1/cover/1a/update.json", + album.cover_update(self.test_photos[1], foo="bar") + mock_post.assert_called_with("/album/1/cover/2b/update.json", foo="bar") self.assertEqual(album.id, "2") self.assertEqual(album.name, "Album 2") @@ -174,44 +175,121 @@ class TestAlbumDelete(TestAlbums): with self.assertRaises(trovebox.TroveboxError): self.test_albums[0].delete() -class TestAlbumAddPhotos(TestAlbums): +class TestAlbumAdd(TestAlbums): @mock.patch.object(trovebox.Trovebox, 'post') - def test_album_add_photos(self, _): - """ If album.add_photos gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.client.album.add_photos(self.test_albums[0], ["Photo Objects"]) + def test_album_add(self, mock_post): + """ Check that photos can be added to an album """ + mock_post.return_value = self._return_value(self.test_albums_dict[1]) + self.client.album.add(self.test_albums[0], self.test_photos, + foo="bar") + mock_post.assert_called_with("/album/1/photo/add.json", + ids=["1a", "2b"], foo="bar") @mock.patch.object(trovebox.Trovebox, 'post') - def test_album_add_photos_id(self, _): - """ If album.add_photos gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.client.album.add_photos("1", ["Photo Objects"]) + def test_album_add_id(self, mock_post): + """ Check that photos can be added to an album using IDs """ + mock_post.return_value = self._return_value(self.test_albums_dict[1]) + self.client.album.add(self.test_albums[0].id, + objects=["1a", "2b"], + object_type="photo", + foo="bar") + mock_post.assert_called_with("/album/1/photo/add.json", + ids=["1a", "2b"], foo="bar") @mock.patch.object(trovebox.Trovebox, 'post') - def test_album_object_add_photos(self, _): - """ If album.add_photos gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.test_albums[0].add_photos(["Photo Objects"]) + def test_album_object_add(self, mock_post): + """ + Check that photos can be added to an album using the + album object directly + """ + mock_post.return_value = self._return_value(self.test_albums_dict[1]) + self.test_albums[0].add(self.test_photos, foo="bar") + mock_post.assert_called_with("/album/1/photo/add.json", + ids=["1a", "2b"], foo="bar") + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_album_add_single(self, mock_post): + """ Check that a single photo can be added to an album """ + mock_post.return_value = self._return_value(self.test_albums_dict[1]) + self.test_albums[0].add(self.test_photos[0], foo="bar") + mock_post.assert_called_with("/album/1/photo/add.json", + ids=["1a"], foo="bar") + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_album_add_invalid_type(self, _): + """ + Check that an exception is raised if an invalid object is added + to an album. + """ + with self.assertRaises(AttributeError): + self.test_albums[0].add([object()]) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_album_add_multiple_types(self, _): + """ + Check that an exception is raised if multiple types are added + to an album. + """ + with self.assertRaises(ValueError): + self.test_albums[0].add(self.test_photos+self.test_albums) class TestAlbumRemovePhotos(TestAlbums): @mock.patch.object(trovebox.Trovebox, 'post') - def test_album_remove_photos(self, _): - """ If album.remove_photos gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.client.album.remove_photos(self.test_albums[0], - ["Photo Objects"]) + def test_album_remove(self, mock_post): + """ Check that photos can be removed from an album """ + mock_post.return_value = self._return_value(self.test_albums_dict[1]) + self.client.album.remove(self.test_albums[0], self.test_photos, + foo="bar") + mock_post.assert_called_with("/album/1/photo/remove.json", + ids=["1a", "2b"], foo="bar") @mock.patch.object(trovebox.Trovebox, 'post') - def test_album_remove_photos_id(self, _): - """ If album.remove_photos gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.client.album.remove_photos("1", ["Photo Objects"]) + def test_album_remove_id(self, mock_post): + """ Check that photos can be removed from an album using IDs """ + mock_post.return_value = self._return_value(self.test_albums_dict[1]) + self.client.album.remove(self.test_albums[0].id, + objects=["1a", "2b"], + object_type="photo", + foo="bar") + mock_post.assert_called_with("/album/1/photo/remove.json", + ids=["1a", "2b"], foo="bar") @mock.patch.object(trovebox.Trovebox, 'post') - def test_album_object_remove_photos(self, _): - """ If album.remove_photos gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.test_albums[0].remove_photos(["Photo Objects"]) + def test_album_object_remove(self, mock_post): + """ + Check that photos can be removed from an album using the + album object directly + """ + mock_post.return_value = self._return_value(self.test_albums_dict[1]) + self.test_albums[0].remove(self.test_photos, foo="bar") + mock_post.assert_called_with("/album/1/photo/remove.json", + ids=["1a", "2b"], foo="bar") + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_album_remove_single(self, mock_post): + """ Check that a single photo can be removed from an album """ + mock_post.return_value = self._return_value(self.test_albums_dict[1]) + self.test_albums[0].remove(self.test_photos[0], foo="bar") + mock_post.assert_called_with("/album/1/photo/remove.json", + ids=["1a"], foo="bar") + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_album_remove_invalid_type(self, _): + """ + Check that an exception is raised if an invalid object is removed + from an album. + """ + with self.assertRaises(AttributeError): + self.test_albums[0].remove([object()]) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_album_remove_multiple_types(self, _): + """ + Check that an exception is raised if multiple types are removed + from an album. + """ + with self.assertRaises(ValueError): + self.test_albums[0].remove(self.test_photos+self.test_albums) class TestAlbumUpdate(TestAlbums): @mock.patch.object(trovebox.Trovebox, 'post') @@ -259,7 +337,7 @@ class TestAlbumView(TestAlbums): self.assertEqual(result.name, "Album 2") self.assertEqual(result.cover.id, "2b") self.assertEqual(result.cover.tags, ["tag3", "tag4"]) - self.assertEqual(result.photos[0].id, self.test_photo.id) + self.assertEqual(result.photos[0].id, self.test_photos[1].id) @mock.patch.object(trovebox.Trovebox, 'get') def test_album_view_id(self, mock_get): @@ -271,7 +349,7 @@ class TestAlbumView(TestAlbums): self.assertEqual(result.name, "Album 2") self.assertEqual(result.cover.id, "2b") self.assertEqual(result.cover.tags, ["tag3", "tag4"]) - self.assertEqual(result.photos[0].id, self.test_photo.id) + self.assertEqual(result.photos[0].id, self.test_photos[1].id) @mock.patch.object(trovebox.Trovebox, 'get') def test_album_object_view(self, mock_get): @@ -284,4 +362,4 @@ class TestAlbumView(TestAlbums): self.assertEqual(album.name, "Album 2") self.assertEqual(album.cover.id, "2b") self.assertEqual(album.cover.tags, ["tag3", "tag4"]) - self.assertEqual(album.photos[0].id, self.test_photo.id) + self.assertEqual(album.photos[0].id, self.test_photos[1].id) diff --git a/trovebox/api/api_action.py b/trovebox/api/api_action.py index f452540..9ffed5f 100644 --- a/trovebox/api/api_action.py +++ b/trovebox/api/api_action.py @@ -2,7 +2,6 @@ api_action.py : Trovebox Action API Classes """ from trovebox.objects.action import Action -from trovebox.objects.photo import Photo from .api_base import ApiBase class ApiAction(ApiBase): @@ -16,12 +15,10 @@ class ApiAction(ApiBase): If a Trovebox object is used, the target type is inferred automatically. """ + # Extract the type from the target if target_type is None: - # Determine the target type - if isinstance(target, Photo): - target_type = "photo" - else: - raise NotImplementedError("Unsupported target type") + target_type = target.get_type() + # Extract the ID from the target try: target_id = target.id diff --git a/trovebox/api/api_activity.py b/trovebox/api/api_activity.py index 2d68f2b..0f3bcea 100644 --- a/trovebox/api/api_activity.py +++ b/trovebox/api/api_activity.py @@ -8,12 +8,12 @@ from .api_base import ApiBase class ApiActivities(ApiBase): """ Definitions of /activities/ API endpoints """ - def list(self, filters={}, **kwds): + def list(self, filters=None, **kwds): """ Endpoint: /activities/[]/list.json Returns a list of Activity objects. - The filters parameter can be used to narrow down the returned activities. + The filters parameter can be used to narrow down the activities. Eg: filters={"type": "photo-upload"} """ filter_string = self._build_filter_string(filters) diff --git a/trovebox/api/api_album.py b/trovebox/api/api_album.py index c3e6d29..b44b9b8 100644 --- a/trovebox/api/api_album.py +++ b/trovebox/api/api_album.py @@ -1,6 +1,9 @@ """ api_album.py : Trovebox Album API Classes """ +import collections + +from trovebox.objects.trovebox_object import TroveboxObject from trovebox.objects.album import Album from trovebox import http from .api_base import ApiBase @@ -53,15 +56,59 @@ class ApiAlbum(ApiBase): album = Album(self._client, {"id": album}) return album.delete(**kwds) - # TODO: Should be just "add" - def add_photos(self, album, photos, **kwds): - """ Not yet implemented """ - raise NotImplementedError() + def add(self, album, objects, object_type=None, **kwds): + """ + Endpoint: /album///add.json - # TODO: Should be just "remove" - def remove_photos(self, album, photos, **kwds): - """ Not yet implemented """ - raise NotImplementedError() + Add objects (eg. Photos) to an album. + The objects are a list of either IDs or Trovebox objects. + If Trovebox objects are used, the object type is inferred + automatically. + Returns True if the album was updated successfully. + """ + return self._add_remove("add", album, objects, object_type, + **kwds) + + def remove(self, album, objects, object_type=None, **kwds): + """ + Endpoint: /album///remove.json + + Remove objects (eg. Photos) to an album. + The objects are a list of either IDs or Trovebox objects. + If Trovebox objects are used, the object type is inferred + automatically. + Returns True if the album was updated successfully. + """ + return self._add_remove("remove", album, objects, object_type, + **kwds) + + def _add_remove(self, action, album, objects, object_type=None, + **kwds): + """Common code for the add and remove endpoints.""" + # Extract the id of the album + if isinstance(album, Album): + album = album.id + + # Ensure we have an iterable of objects + if not isinstance(objects, collections.Iterable): + objects = [objects] + + # Extract the type of the objects + if object_type is None: + object_type = objects[0].get_type() + + for i, obj in enumerate(objects): + if isinstance(obj, TroveboxObject): + # Ensure all objects are the same type + if obj.get_type() != object_type: + raise ValueError("Not all objects are of type '%s'" + % object_type) + # Extract the ids of the objects + objects[i] = obj.id + + return self._client.post("/album/%s/%s/%s.json" % + (album, object_type, action), + ids=objects, **kwds)["result"] def update(self, album, **kwds): """ diff --git a/trovebox/api/api_base.py b/trovebox/api/api_base.py index ef462bd..553a4c7 100644 --- a/trovebox/api/api_base.py +++ b/trovebox/api/api_base.py @@ -3,6 +3,7 @@ api_base.py: Base class for all API classes """ class ApiBase(object): + """ Base class for all API objects """ def __init__(self, client): self._client = client @@ -13,6 +14,7 @@ class ApiBase(object): :returns: filter_string formatted for an API endpoint """ filter_string = "" - for filter in filters: - filter_string += "%s-%s/" % (filter, filters[filter]) + if filters is not None: + for filt in filters: + filter_string += "%s-%s/" % (filt, filters[filt]) return filter_string diff --git a/trovebox/objects/action.py b/trovebox/objects/action.py index e7c9ce3..6bee463 100644 --- a/trovebox/objects/action.py +++ b/trovebox/objects/action.py @@ -11,6 +11,7 @@ class Action(TroveboxObject): self.target = None self.target_type = None TroveboxObject.__init__(self, trovebox, json_dict) + self._type = "action" self._update_fields_with_objects() def _update_fields_with_objects(self): diff --git a/trovebox/objects/activity.py b/trovebox/objects/activity.py index fae7605..2831e1d 100644 --- a/trovebox/objects/activity.py +++ b/trovebox/objects/activity.py @@ -12,6 +12,7 @@ class Activity(TroveboxObject): self.data = None self.type = None TroveboxObject.__init__(self, trovebox, json_dict) + self._type = "activity" self._update_fields_with_objects() def _update_fields_with_objects(self): diff --git a/trovebox/objects/album.py b/trovebox/objects/album.py index 570aa37..7715644 100644 --- a/trovebox/objects/album.py +++ b/trovebox/objects/album.py @@ -11,18 +11,25 @@ class Album(TroveboxObject): self.photos = None self.cover = None TroveboxObject.__init__(self, trovebox, json_dict) + self._type = "album" self._update_fields_with_objects() def _update_fields_with_objects(self): """ Convert dict fields into objects, where appropriate """ # Update the cover with a photo object - if isinstance(self.cover, dict): - self.cover = Photo(self._trovebox, self.cover) + try: + if isinstance(self.cover, dict): + self.cover = Photo(self._trovebox, self.cover) + except AttributeError: + pass # No cover + # Update the photo list with photo objects - if isinstance(self.photos, list): + try: for i, photo in enumerate(self.photos): if isinstance(photo, dict): self.photos[i] = Photo(self._trovebox, photo) + except (AttributeError, TypeError): + pass # No photos, or not a list def cover_update(self, photo, **kwds): """ @@ -60,15 +67,45 @@ class Album(TroveboxObject): self._delete_fields() return result - # TODO: Should be just "add" - def add_photos(self, photos, **kwds): - """ Not implemented yet """ - raise NotImplementedError() + def add(self, objects, object_type=None, **kwds): + """ + Endpoint: /album///add.json - # TODO: Should be just "remove" - def remove_photos(self, photos, **kwds): - """ Not implemented yet """ - raise NotImplementedError() + Add objects (eg. Photos) to this album. + The objects are a list of either IDs or Trovebox objects. + If Trovebox objects are used, the object type is inferred + automatically. + Updates the album's fields with the response. + """ + result = self._trovebox.album.add(self, objects, object_type, **kwds) + + # API currently doesn't return the updated album + # (frontend issue #1369) + if isinstance(result, bool): # pragma: no cover + result = self._trovebox.get("/album/%s/view.json" % + self.id)["result"] + self._replace_fields(result) + self._update_fields_with_objects() + + def remove(self, objects, object_type=None, **kwds): + """ + Endpoint: /album///remove.json + + Remove objects (eg. Photos) from this album. + The objects are a list of either IDs or Trovebox objects. + If Trovebox objects are used, the object type is inferred + automatically. + Updates the album's fields with the response. + """ + result = self._trovebox.album.remove(self, objects, object_type, + **kwds) + # API currently doesn't return the updated album + # (frontend issue #1369) + if isinstance(result, bool): # pragma: no cover + result = self._trovebox.get("/album/%s/view.json" % + self.id)["result"] + self._replace_fields(result) + self._update_fields_with_objects() def update(self, **kwds): """ diff --git a/trovebox/objects/photo.py b/trovebox/objects/photo.py index 03e7435..5ff20a2 100644 --- a/trovebox/objects/photo.py +++ b/trovebox/objects/photo.py @@ -6,6 +6,10 @@ from .trovebox_object import TroveboxObject class Photo(TroveboxObject): """ Representation of a Photo object """ + def __init__(self, trovebox, json_dict): + TroveboxObject.__init__(self, trovebox, json_dict) + self._type = "photo" + def delete(self, **kwds): """ Endpoint: /photo//delete.json diff --git a/trovebox/objects/tag.py b/trovebox/objects/tag.py index 5c9b905..f6841c1 100644 --- a/trovebox/objects/tag.py +++ b/trovebox/objects/tag.py @@ -11,6 +11,10 @@ from .trovebox_object import TroveboxObject class Tag(TroveboxObject): """ Representation of a Tag object """ + def __init__(self, trovebox, json_dict): + TroveboxObject.__init__(self, trovebox, json_dict) + self._type = "tag" + def delete(self, **kwds): """ Endpoint: /tag//delete.json diff --git a/trovebox/objects/trovebox_object.py b/trovebox/objects/trovebox_object.py index 86284dc..d3c34dd 100644 --- a/trovebox/objects/trovebox_object.py +++ b/trovebox/objects/trovebox_object.py @@ -4,6 +4,7 @@ Base object supporting the storage of custom fields as attributes class TroveboxObject(object): """ Base object supporting the storage of custom fields as attributes """ def __init__(self, trovebox, json_dict): + self._type = "None" self.id = None self.name = None self._trovebox = trovebox @@ -48,3 +49,7 @@ class TroveboxObject(object): def get_fields(self): """ Returns this object's attributes """ return self._json_dict + + def get_type(self): + """ Return this object's type (eg. "photo") """ + return self._type From 3cfc090dd02f627f7b7e201d2671e56b6022f4d0 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 8 Sep 2013 17:27:58 +0100 Subject: [PATCH 44/80] Pylint fixes --- trovebox/.pylint-ignores.patch | 162 ++++++++++++++------------------- trovebox/__init__.py | 2 +- trovebox/_version.py | 2 +- trovebox/http.py | 2 +- trovebox/main.py | 5 +- 5 files changed, 75 insertions(+), 98 deletions(-) diff --git a/trovebox/.pylint-ignores.patch b/trovebox/.pylint-ignores.patch index 3ab31d9..24996b7 100644 --- a/trovebox/.pylint-ignores.patch +++ b/trovebox/.pylint-ignores.patch @@ -1,48 +1,60 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_activity.py patched/api/api_activity.py ---- original/api/api_activity.py 2013-09-02 21:17:41.848947000 +0100 -+++ patched/api/api_activity.py 2013-09-02 21:18:19.701134833 +0100 -@@ -22,7 +22,7 @@ +--- original/api/api_activity.py ++++ patched/api/api_activity.py +@@ -33,7 +33,7 @@ raise TroveboxError("Purge response returned False") return True --class ApiActivity(object): -+class ApiActivity(object): # pylint: disable=R0903 +-class ApiActivity(ApiBase): ++class ApiActivity(ApiBase): # pylint: disable=too-few-public-methods """ Definitions of /activity/ API endpoints """ - def __init__(self, client): - self._client = client + def view(self, activity, **kwds): + """ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_album.py patched/api/api_album.py ---- original/api/api_album.py 2013-09-02 21:17:41.848947000 +0100 -+++ patched/api/api_album.py 2013-09-02 21:18:19.701134833 +0100 -@@ -4,7 +4,7 @@ - from trovebox.objects.album import Album +--- original/api/api_album.py ++++ patched/api/api_album.py +@@ -8,7 +8,7 @@ from trovebox import http + from .api_base import ApiBase --class ApiAlbums(object): -+class ApiAlbums(object): # pylint: disable=R0903 +-class ApiAlbums(ApiBase): ++class ApiAlbums(ApiBase): # pylint: disable=too-few-public-methods """ Definitions of /albums/ API endpoints """ + def list(self, **kwds): + """ +diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_base.py patched/api/api_base.py +--- original/api/api_base.py ++++ patched/api/api_base.py +@@ -2,7 +2,7 @@ + api_base.py: Base class for all API classes + """ + +-class ApiBase(object): ++class ApiBase(object): # pylint: disable=too-few-public-methods + """ Base class for all API objects """ def __init__(self, client): self._client = client diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_tag.py patched/api/api_tag.py ---- original/api/api_tag.py 2013-09-02 21:17:41.848947000 +0100 -+++ patched/api/api_tag.py 2013-09-02 21:18:19.705134853 +0100 -@@ -4,7 +4,7 @@ - from trovebox import http +--- original/api/api_tag.py ++++ patched/api/api_tag.py +@@ -5,7 +5,7 @@ from trovebox.objects.tag import Tag + from .api_base import ApiBase --class ApiTags(object): -+class ApiTags(object): # pylint: disable=R0903 +-class ApiTags(ApiBase): ++class ApiTags(ApiBase): # pylint: disable=too-few-public-methods """ Definitions of /tags/ API endpoints """ - def __init__(self, client): - self._client = client + def list(self, **kwds): + """ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/auth.py patched/auth.py ---- original/auth.py 2013-09-02 21:17:41.848947000 +0100 -+++ patched/auth.py 2013-09-02 21:18:19.705134853 +0100 +--- original/auth.py ++++ patched/auth.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import os try: - from configparser import ConfigParser # Python3 -+ from configparser import ConfigParser # Python3 # pylint: disable=F0401 ++ from configparser import ConfigParser # Python3 # pylint: disable=import-error except ImportError: from ConfigParser import SafeConfigParser as ConfigParser # Python2 try: @@ -51,10 +63,10 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/auth.py pa import StringIO as io # Python2 -class Auth(object): -+class Auth(object): # pylint: disable=R0903 ++class Auth(object): # pylint: disable=too-few-public-methods """OAuth secrets""" - def __init__(self, config_file, host, -+ def __init__(self, config_file, host, # pylint: disable=R0913 ++ def __init__(self, config_file, host, # pylint: disable=too-many-arguments consumer_key, consumer_secret, token, token_secret): if host is None: @@ -63,33 +75,31 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/auth.py pa parser.optionxform = str # Case-sensitive options try: - parser.read_file(buf) # Python3 -+ parser.read_file(buf) # Python3 # pylint: disable=E1103 ++ parser.read_file(buf) # Python3 # pylint: disable=maybe-no-member except AttributeError: parser.readfp(buf) # Python2 diff --unified --recursive '--exclude=.pylint-ignores.patch' original/http.py patched/http.py ---- original/http.py 2013-09-02 21:17:41.848947000 +0100 -+++ patched/http.py 2013-09-02 21:18:25.749164824 +0100 -@@ -7,18 +7,18 @@ +--- original/http.py ++++ patched/http.py +@@ -7,7 +7,7 @@ import requests_oauthlib import logging try: - from urllib.parse import urlparse, urlunparse # Python3 -+ from urllib.parse import urlparse, urlunparse # Python3 # pylint: disable=F0401,E0611 ++ from urllib.parse import urlparse, urlunparse # Python3 # pylint: disable=import-error,no-name-in-module except ImportError: from urlparse import urlparse, urlunparse # Python2 - from trovebox.objects.trovebox_object import TroveboxObject --from .errors import * -+from .errors import * # pylint: disable=W0401 +@@ -16,9 +16,9 @@ from .auth import Auth if sys.version < '3': - TEXT_TYPE = unicode -+ TEXT_TYPE = unicode # pylint: disable=C0103 ++ TEXT_TYPE = unicode # pylint: disable=invalid-name else: # pragma: no cover - TEXT_TYPE = str -+ TEXT_TYPE = str # pylint: disable=C0103 ++ TEXT_TYPE = str # pylint: disable=invalid-name DUPLICATE_RESPONSE = {"code": 409, "message": "This photo already exists"} @@ -98,28 +108,19 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/http.py pa } - def __init__(self, config_file=None, host=None, -+ def __init__(self, config_file=None, host=None, # pylint: disable=R0913 ++ def __init__(self, config_file=None, host=None, # pylint: disable=too-many-arguments consumer_key='', consumer_secret='', token='', token_secret='', api_version=None): diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.py patched/__init__.py ---- original/__init__.py 2013-09-02 21:17:41.848947000 +0100 -+++ patched/__init__.py 2013-09-02 21:18:19.705134853 +0100 -@@ -2,7 +2,7 @@ - __init__.py : Trovebox package top level - """ - from .http import Http --from .errors import * -+from .errors import * # pylint: disable=W0401 - from ._version import __version__ - from trovebox.api import api_photo - from trovebox.api import api_tag +--- original/__init__.py ++++ patched/__init__.py @@ -12,7 +12,7 @@ LATEST_API_VERSION = 2 -class Trovebox(Http): -+class Trovebox(Http): # pylint: disable=R0902 ++class Trovebox(Http): # pylint: disable=too-many-instance-attributes """ Client library for Trovebox If no parameters are specified, config is loaded from the default @@ -128,82 +129,57 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.p even if the Trovebox API is updated to a new revision. """ - def __init__(self, config_file=None, host=None, -+ def __init__(self, config_file=None, host=None, # pylint: disable=R0913 ++ def __init__(self, config_file=None, host=None, # pylint: disable=too-many-arguments consumer_key='', consumer_secret='', token='', token_secret='', api_version=None): diff --unified --recursive '--exclude=.pylint-ignores.patch' original/main.py patched/main.py ---- original/main.py 2013-09-02 21:17:41.852947000 +0100 -+++ patched/main.py 2013-09-02 21:18:19.705134853 +0100 +--- original/main.py ++++ patched/main.py @@ -26,7 +26,7 @@ ################################################################# -def main(args=sys.argv[1:]): -+def main(args=sys.argv[1:]): # pylint: disable=R0912,C0111 ++def main(args=sys.argv[1:]): # pylint: disable=too-many-branches + """Run the commandline script""" usage = "%prog --help" parser = OptionParser(usage, add_help_option=False) - parser.add_option('-c', '--config', help="Configuration file to use", -@@ -84,13 +84,13 @@ +@@ -85,11 +85,11 @@ sys.exit(1) if options.method == "GET": - result = client.get(options.endpoint, process_response=False, -+ result = client.get(options.endpoint, process_response=False, # pylint: disable=W0142 ++ result = client.get(options.endpoint, process_response=False, # pylint: disable=star-args **params) else: params, files = extract_files(params) - result = client.post(options.endpoint, process_response=False, -+ result = client.post(options.endpoint, process_response=False, # pylint: disable=W0142 ++ result = client.post(options.endpoint, process_response=False, # pylint: disable=star-args files=files, **params) -- for f in files: -+ for f in files: # pylint: disable=C0103 - files[f].close() - - if options.verbose: -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/photo.py patched/objects/photo.py ---- original/objects/photo.py 2013-09-02 19:48:44.862482000 +0100 -+++ patched/objects/photo.py 2013-09-02 21:18:29.001180950 +0100 -@@ -1,4 +1,4 @@ --""" -+""" # pylint: disable=R0801 - Representation of a Photo object - """ - from trovebox.errors import TroveboxError + for file_ in files: + files[file_].close() diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/tag.py patched/objects/tag.py ---- original/objects/tag.py 2013-09-02 21:17:41.852947000 +0100 -+++ patched/objects/tag.py 2013-09-02 21:18:19.705134853 +0100 -@@ -1,8 +1,8 @@ --""" -+""" # pylint: disable=R0801 +--- original/objects/tag.py ++++ patched/objects/tag.py +@@ -2,7 +2,7 @@ Representation of a Tag object """ try: - from urllib.parse import quote # Python3 -+ from urllib.parse import quote # Python3 # pylint: disable=F0401,E0611 ++ from urllib.parse import quote # Python3 # pylint: disable=import-error,no-name-in-module except ImportError: from urllib import quote # Python2 diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/trovebox_object.py patched/objects/trovebox_object.py ---- original/objects/trovebox_object.py 2013-09-02 21:17:41.852947000 +0100 -+++ patched/objects/trovebox_object.py 2013-09-02 21:18:19.705134853 +0100 -@@ -1,10 +1,10 @@ - """ - Base object supporting the storage of custom fields as attributes - """ --class TroveboxObject(object): -+class TroveboxObject(object): # pylint: disable=R0903 +--- original/objects/trovebox_object.py ++++ patched/objects/trovebox_object.py +@@ -5,7 +5,7 @@ """ Base object supporting the storage of custom fields as attributes """ def __init__(self, trovebox, json_dict): + self._type = "None" - self.id = None -+ self.id = None # pylint: disable=C0103 ++ self.id = None # pylint: disable=invalid-name self.name = None self._trovebox = trovebox self._json_dict = json_dict -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/_version.py patched/_version.py ---- original/_version.py 2013-09-02 21:17:41.852947000 +0100 -+++ patched/_version.py 2013-09-02 21:18:19.705134853 +0100 -@@ -1,2 +1,2 @@ -- -+ # pylint: disable=C0111 - __version__ = "0.5" diff --git a/trovebox/__init__.py b/trovebox/__init__.py index 9189d35..73e2af5 100644 --- a/trovebox/__init__.py +++ b/trovebox/__init__.py @@ -2,7 +2,7 @@ __init__.py : Trovebox package top level """ from .http import Http -from .errors import * +from .errors import TroveboxError, TroveboxDuplicateError, Trovebox404Error from ._version import __version__ from trovebox.api import api_photo from trovebox.api import api_tag diff --git a/trovebox/_version.py b/trovebox/_version.py index ad57014..baa0f41 100644 --- a/trovebox/_version.py +++ b/trovebox/_version.py @@ -1,2 +1,2 @@ - +"""Current version string""" __version__ = "0.5" diff --git a/trovebox/http.py b/trovebox/http.py index ffeb487..30e342e 100644 --- a/trovebox/http.py +++ b/trovebox/http.py @@ -12,7 +12,7 @@ except ImportError: from urlparse import urlparse, urlunparse # Python2 from trovebox.objects.trovebox_object import TroveboxObject -from .errors import * +from .errors import TroveboxError, Trovebox404Error, TroveboxDuplicateError from .auth import Auth if sys.version < '3': diff --git a/trovebox/main.py b/trovebox/main.py index b9b9f4f..081597c 100644 --- a/trovebox/main.py +++ b/trovebox/main.py @@ -27,6 +27,7 @@ To get your credentials: ################################################################# def main(args=sys.argv[1:]): + """Run the commandline script""" usage = "%prog --help" parser = OptionParser(usage, add_help_option=False) parser.add_option('-c', '--config', help="Configuration file to use", @@ -90,8 +91,8 @@ def main(args=sys.argv[1:]): params, files = extract_files(params) result = client.post(options.endpoint, process_response=False, files=files, **params) - for f in files: - files[f].close() + for file_ in files: + files[file_].close() if options.verbose: print("==========\nMethod: %s\nHost: %s\nEndpoint: %s" % From 7a7b43afc715d746284e71c5be06c94086bf5c93 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Mon, 9 Sep 2013 18:08:11 +0100 Subject: [PATCH 45/80] Renamed ._trovebox to ._client, to match API classes. Changed _type to a class attribute. --- trovebox/objects/action.py | 17 ++++----- trovebox/objects/activity.py | 13 +++---- trovebox/objects/album.py | 53 +++++++++++++++-------------- trovebox/objects/photo.py | 32 ++++++++--------- trovebox/objects/tag.py | 12 +++---- trovebox/objects/trovebox_object.py | 6 ++-- 6 files changed, 66 insertions(+), 67 deletions(-) diff --git a/trovebox/objects/action.py b/trovebox/objects/action.py index 6bee463..847a12c 100644 --- a/trovebox/objects/action.py +++ b/trovebox/objects/action.py @@ -7,11 +7,12 @@ from .photo import Photo class Action(TroveboxObject): """ Representation of an Action object """ - def __init__(self, trovebox, json_dict): + _type = "action" + + def __init__(self, client, json_dict): self.target = None self.target_type = None - TroveboxObject.__init__(self, trovebox, json_dict) - self._type = "action" + TroveboxObject.__init__(self, client, json_dict) self._update_fields_with_objects() def _update_fields_with_objects(self): @@ -19,7 +20,7 @@ class Action(TroveboxObject): # Update the photo target with photo objects if self.target is not None: if self.target_type == "photo": - self.target = Photo(self._trovebox, self.target) + self.target = Photo(self._client, self.target) else: raise NotImplementedError("Actions can only be assigned to " "Photos") @@ -32,8 +33,8 @@ class Action(TroveboxObject): Returns True if successful. Raises a TroveboxError if not. """ - result = self._trovebox.post("/action/%s/delete.json" % - self.id, **kwds)["result"] + result = self._client.post("/action/%s/delete.json" % + self.id, **kwds)["result"] if not result: raise TroveboxError("Delete response returned False") self._delete_fields() @@ -46,7 +47,7 @@ class Action(TroveboxObject): Requests the full contents of the action. Updates the action's fields with the response. """ - result = self._trovebox.get("/action/%s/view.json" % - self.id, **kwds)["result"] + result = self._client.get("/action/%s/view.json" % + self.id, **kwds)["result"] self._replace_fields(result) self._update_fields_with_objects() diff --git a/trovebox/objects/activity.py b/trovebox/objects/activity.py index 2831e1d..1aeffc2 100644 --- a/trovebox/objects/activity.py +++ b/trovebox/objects/activity.py @@ -8,11 +8,12 @@ from .photo import Photo class Activity(TroveboxObject): """ Representation of an Activity object """ - def __init__(self, trovebox, json_dict): + _type = "activity" + + def __init__(self, client, json_dict): self.data = None self.type = None - TroveboxObject.__init__(self, trovebox, json_dict) - self._type = "activity" + TroveboxObject.__init__(self, client, json_dict) self._update_fields_with_objects() def _update_fields_with_objects(self): @@ -20,7 +21,7 @@ class Activity(TroveboxObject): # Update the data with photo objects if self.type is not None: if self.type.startswith("photo"): - self.data = Photo(self._trovebox, self.data) + self.data = Photo(self._client, self.data) else: raise NotImplementedError("Unrecognised activity type: %s" % self.type) @@ -32,8 +33,8 @@ class Activity(TroveboxObject): Requests the full contents of the activity. Updates the activity's fields with the response. """ - result = self._trovebox.get("/activity/%s/view.json" % - self.id, **kwds)["result"] + result = self._client.get("/activity/%s/view.json" % + self.id, **kwds)["result"] # TBD: Why is the result enclosed/encoded like this? result = result["0"] diff --git a/trovebox/objects/album.py b/trovebox/objects/album.py index 7715644..4a65cc5 100644 --- a/trovebox/objects/album.py +++ b/trovebox/objects/album.py @@ -7,11 +7,12 @@ from .photo import Photo class Album(TroveboxObject): """ Representation of an Album object """ - def __init__(self, trovebox, json_dict): + _type = "album" + + def __init__(self, client, json_dict): self.photos = None self.cover = None - TroveboxObject.__init__(self, trovebox, json_dict) - self._type = "album" + TroveboxObject.__init__(self, client, json_dict) self._update_fields_with_objects() def _update_fields_with_objects(self): @@ -19,7 +20,7 @@ class Album(TroveboxObject): # Update the cover with a photo object try: if isinstance(self.cover, dict): - self.cover = Photo(self._trovebox, self.cover) + self.cover = Photo(self._client, self.cover) except AttributeError: pass # No cover @@ -27,7 +28,7 @@ class Album(TroveboxObject): try: for i, photo in enumerate(self.photos): if isinstance(photo, dict): - self.photos[i] = Photo(self._trovebox, photo) + self.photos[i] = Photo(self._client, photo) except (AttributeError, TypeError): pass # No photos, or not a list @@ -38,17 +39,17 @@ class Album(TroveboxObject): Update the cover photo of this album. """ if not isinstance(photo, Photo): - photo = Photo(self._trovebox, {"id": photo}) + photo = Photo(self._client, {"id": photo}) - result = self._trovebox.post("/album/%s/cover/%s/update.json" % - (self.id, photo.id), - **kwds)["result"] + result = self._client.post("/album/%s/cover/%s/update.json" % + (self.id, photo.id), + **kwds)["result"] # API currently doesn't return the updated album # (frontend issue #1369) if isinstance(result, bool): # pragma: no cover - result = self._trovebox.get("/album/%s/view.json" % - self.id)["result"] + result = self._client.get("/album/%s/view.json" % + self.id)["result"] self._replace_fields(result) self._update_fields_with_objects() @@ -60,8 +61,8 @@ class Album(TroveboxObject): Returns True if successful. Raises a TroveboxError if not. """ - result = self._trovebox.post("/album/%s/delete.json" % - self.id, **kwds)["result"] + result = self._client.post("/album/%s/delete.json" % + self.id, **kwds)["result"] if not result: raise TroveboxError("Delete response returned False") self._delete_fields() @@ -77,13 +78,13 @@ class Album(TroveboxObject): automatically. Updates the album's fields with the response. """ - result = self._trovebox.album.add(self, objects, object_type, **kwds) + result = self._client.album.add(self, objects, object_type, **kwds) # API currently doesn't return the updated album # (frontend issue #1369) if isinstance(result, bool): # pragma: no cover - result = self._trovebox.get("/album/%s/view.json" % - self.id)["result"] + result = self._client.get("/album/%s/view.json" % + self.id)["result"] self._replace_fields(result) self._update_fields_with_objects() @@ -97,13 +98,13 @@ class Album(TroveboxObject): automatically. Updates the album's fields with the response. """ - result = self._trovebox.album.remove(self, objects, object_type, - **kwds) + result = self._client.album.remove(self, objects, object_type, + **kwds) # API currently doesn't return the updated album # (frontend issue #1369) if isinstance(result, bool): # pragma: no cover - result = self._trovebox.get("/album/%s/view.json" % - self.id)["result"] + result = self._client.get("/album/%s/view.json" % + self.id)["result"] self._replace_fields(result) self._update_fields_with_objects() @@ -113,13 +114,13 @@ class Album(TroveboxObject): Updates this album with the specified parameters. """ - result = self._trovebox.post("/album/%s/update.json" % - self.id, **kwds)["result"] + result = self._client.post("/album/%s/update.json" % + self.id, **kwds)["result"] # APIv1 doesn't return the updated album (frontend issue #937) if isinstance(result, bool): # pragma: no cover - result = self._trovebox.get("/album/%s/view.json" % - self.id)["result"] + result = self._client.get("/album/%s/view.json" % + self.id)["result"] self._replace_fields(result) self._update_fields_with_objects() @@ -131,7 +132,7 @@ class Album(TroveboxObject): Requests all properties of an album. Updates the album's fields with the response. """ - result = self._trovebox.get("/album/%s/view.json" % - self.id, **kwds)["result"] + result = self._client.get("/album/%s/view.json" % + self.id, **kwds)["result"] self._replace_fields(result) self._update_fields_with_objects() diff --git a/trovebox/objects/photo.py b/trovebox/objects/photo.py index 5ff20a2..2b6fc5c 100644 --- a/trovebox/objects/photo.py +++ b/trovebox/objects/photo.py @@ -6,9 +6,7 @@ from .trovebox_object import TroveboxObject class Photo(TroveboxObject): """ Representation of a Photo object """ - def __init__(self, trovebox, json_dict): - TroveboxObject.__init__(self, trovebox, json_dict) - self._type = "photo" + _type = "photo" def delete(self, **kwds): """ @@ -18,8 +16,8 @@ class Photo(TroveboxObject): Returns True if successful. Raises a TroveboxError if not. """ - result = self._trovebox.post("/photo/%s/delete.json" % - self.id, **kwds)["result"] + result = self._client.post("/photo/%s/delete.json" % + self.id, **kwds)["result"] if not result: raise TroveboxError("Delete response returned False") self._delete_fields() @@ -41,8 +39,8 @@ class Photo(TroveboxObject): Updates this photo with the specified parameters. """ - result = self._trovebox.post("/photo/%s/update.json" % - self.id, **kwds)["result"] + result = self._client.post("/photo/%s/update.json" % + self.id, **kwds)["result"] self._replace_fields(result) # TODO: Add options @@ -55,8 +53,8 @@ class Photo(TroveboxObject): by using the "returnSizes" parameter. Updates the photo's fields with the response. """ - result = self._trovebox.get("/photo/%s/view.json" % - self.id, **kwds)["result"] + result = self._client.get("/photo/%s/view.json" % + self.id, **kwds)["result"] self._replace_fields(result) def dynamic_url(self, **kwds): @@ -71,8 +69,8 @@ class Photo(TroveboxObject): Returns a dict containing the next and previous photo lists (there may be more than one next/previous photo returned). """ - result = self._trovebox.get("/photo/%s/nextprevious.json" % - self.id, **kwds)["result"] + result = self._client.get("/photo/%s/nextprevious.json" % + self.id, **kwds)["result"] value = {} if "next" in result: # Workaround for APIv1 @@ -81,7 +79,7 @@ class Photo(TroveboxObject): value["next"] = [] for photo in result["next"]: - value["next"].append(Photo(self._trovebox, photo)) + value["next"].append(Photo(self._client, photo)) if "previous" in result: # Workaround for APIv1 @@ -90,7 +88,7 @@ class Photo(TroveboxObject): value["previous"] = [] for photo in result["previous"]: - value["previous"].append(Photo(self._trovebox, photo)) + value["previous"].append(Photo(self._client, photo)) return value @@ -102,12 +100,12 @@ class Photo(TroveboxObject): eg. transform(photo, rotate=90) Updates the photo's fields with the response. """ - result = self._trovebox.post("/photo/%s/transform.json" % - self.id, **kwds)["result"] + result = self._client.post("/photo/%s/transform.json" % + self.id, **kwds)["result"] # APIv1 doesn't return the transformed photo (frontend issue #955) if isinstance(result, bool): # pragma: no cover - result = self._trovebox.get("/photo/%s/view.json" % - self.id)["result"] + result = self._client.get("/photo/%s/view.json" % + self.id)["result"] self._replace_fields(result) diff --git a/trovebox/objects/tag.py b/trovebox/objects/tag.py index f6841c1..159ec51 100644 --- a/trovebox/objects/tag.py +++ b/trovebox/objects/tag.py @@ -11,9 +11,7 @@ from .trovebox_object import TroveboxObject class Tag(TroveboxObject): """ Representation of a Tag object """ - def __init__(self, trovebox, json_dict): - TroveboxObject.__init__(self, trovebox, json_dict) - self._type = "tag" + _type = "tag" def delete(self, **kwds): """ @@ -23,8 +21,8 @@ class Tag(TroveboxObject): Returns True if successful. Raises a TroveboxError if not. """ - result = self._trovebox.post("/tag/%s/delete.json" % - quote(self.id), **kwds)["result"] + result = self._client.post("/tag/%s/delete.json" % + quote(self.id), **kwds)["result"] if not result: raise TroveboxError("Delete response returned False") self._delete_fields() @@ -37,8 +35,8 @@ class Tag(TroveboxObject): Updates this tag with the specified parameters. Returns the updated tag object. """ - result = self._trovebox.post("/tag/%s/update.json" % quote(self.id), - **kwds)["result"] + result = self._client.post("/tag/%s/update.json" % quote(self.id), + **kwds)["result"] self._replace_fields(result) # def view(self, **kwds): diff --git a/trovebox/objects/trovebox_object.py b/trovebox/objects/trovebox_object.py index d3c34dd..b7132bc 100644 --- a/trovebox/objects/trovebox_object.py +++ b/trovebox/objects/trovebox_object.py @@ -3,11 +3,11 @@ Base object supporting the storage of custom fields as attributes """ class TroveboxObject(object): """ Base object supporting the storage of custom fields as attributes """ - def __init__(self, trovebox, json_dict): - self._type = "None" + _type = "None" + def __init__(self, client, json_dict): self.id = None self.name = None - self._trovebox = trovebox + self._client = client self._json_dict = json_dict self._set_fields(json_dict) From 86ba0914c8f0b5b68c5d6253055a0233beb510bd Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Tue, 10 Sep 2013 17:45:53 +0100 Subject: [PATCH 46/80] Refactor the smarts into the api classes. The object classes are now simple wrappers. Improve parameter testing, by passing foo="bar" where possible. --- tests/unit/test_actions.py | 16 +++---- tests/unit/test_activities.py | 18 +++---- tests/unit/test_albums.py | 67 +++++++++++++++------------ tests/unit/test_photos.py | 68 ++++++++++++++++----------- tests/unit/test_tags.py | 29 ++++++------ trovebox/.pylint-ignores.patch | 31 ++++++------- trovebox/api/api_action.py | 23 +++++---- trovebox/api/api_activity.py | 16 ++++--- trovebox/api/api_album.py | 73 ++++++++++++++++++----------- trovebox/api/api_base.py | 18 +++++++ trovebox/api/api_photo.py | 85 ++++++++++++++++++++-------------- trovebox/api/api_tag.py | 26 +++++++---- trovebox/http.py | 9 ---- trovebox/objects/action.py | 13 ++---- trovebox/objects/activity.py | 13 ++---- trovebox/objects/album.py | 52 ++++----------------- trovebox/objects/photo.py | 50 ++++---------------- trovebox/objects/tag.py | 16 ++----- 18 files changed, 307 insertions(+), 316 deletions(-) diff --git a/tests/unit/test_actions.py b/tests/unit/test_actions.py index cbbb6b2..8da4bb6 100644 --- a/tests/unit/test_actions.py +++ b/tests/unit/test_actions.py @@ -71,7 +71,7 @@ class TestActionCreate(TestActions): invalid object. """ with self.assertRaises(AttributeError): - self.client.action.create(target=object(), foo="bar") + self.client.action.create(target=object()) @mock.patch.object(trovebox.Trovebox, 'post') def test_action_create_invalid_return_type(self, mock_post): @@ -79,23 +79,23 @@ class TestActionCreate(TestActions): mock_post.return_value = self._return_value({"target": "test", "target_type": "invalid"}) with self.assertRaises(NotImplementedError): - self.client.action.create(target=self.test_photos[0], foo="bar") + self.client.action.create(target=self.test_photos[0]) class TestActionDelete(TestActions): @mock.patch.object(trovebox.Trovebox, 'post') def test_action_delete(self, mock_post): """Check that an action can be deleted""" mock_post.return_value = self._return_value(True) - result = self.client.action.delete(self.test_actions[0]) - mock_post.assert_called_with("/action/1/delete.json") + result = self.client.action.delete(self.test_actions[0], foo="bar") + mock_post.assert_called_with("/action/1/delete.json", foo="bar") self.assertEqual(result, True) @mock.patch.object(trovebox.Trovebox, 'post') def test_action_delete_id(self, mock_post): """Check that an action can be deleted using its ID""" mock_post.return_value = self._return_value(True) - result = self.client.action.delete("1") - mock_post.assert_called_with("/action/1/delete.json") + result = self.client.action.delete("1", foo="bar") + mock_post.assert_called_with("/action/1/delete.json", foo="bar") self.assertEqual(result, True) @mock.patch.object(trovebox.Trovebox, 'post') @@ -110,8 +110,8 @@ class TestActionDelete(TestActions): """Check that an action can be deleted using the action object directly""" mock_post.return_value = self._return_value(True) action = self.test_actions[0] - result = action.delete() - mock_post.assert_called_with("/action/1/delete.json") + result = action.delete(foo="bar") + mock_post.assert_called_with("/action/1/delete.json", foo="bar") self.assertEqual(result, True) self.assertEqual(action.get_fields(), {}) self.assertEqual(action.id, None) diff --git a/tests/unit/test_activities.py b/tests/unit/test_activities.py index 29e3afa..d756ff6 100644 --- a/tests/unit/test_activities.py +++ b/tests/unit/test_activities.py @@ -42,8 +42,8 @@ class TestActivitiesList(TestActivities): """Check that the activity list is returned correctly""" mock_get.return_value = self._return_value(self.test_activities_dict) - result = self.client.activities.list() - mock_get.assert_called_with("/activities/list.json") + result = self.client.activities.list(foo="bar") + mock_get.assert_called_with("/activities/list.json", foo="bar") self.assertEqual(len(result), 2) self.assertEqual(result[0].id, "1") self.assertEqual(result[0].type, "photo_upload") @@ -56,16 +56,16 @@ class TestActivitiesList(TestActivities): def test_empty_result(self, mock_get): """Check that an empty result is transformed into an empty list """ mock_get.return_value = self._return_value("") - result = self.client.activities.list() - mock_get.assert_called_with("/activities/list.json") + result = self.client.activities.list(foo="bar") + mock_get.assert_called_with("/activities/list.json", foo="bar") self.assertEqual(result, []) @mock.patch.object(trovebox.Trovebox, 'get') def test_zero_rows(self, mock_get): """Check that totalRows=0 is transformed into an empty list """ mock_get.return_value = self._return_value([{"totalRows": 0}]) - result = self.client.activities.list() - mock_get.assert_called_with("/activities/list.json") + result = self.client.activities.list(foo="bar") + mock_get.assert_called_with("/activities/list.json", foo="bar") self.assertEqual(result, []) @mock.patch.object(trovebox.Trovebox, 'get') @@ -73,11 +73,13 @@ class TestActivitiesList(TestActivities): """Check that the activity list filters are applied properly""" mock_get.return_value = self._return_value(self.test_activities_dict) self.client.activities.list(filters={"foo": "bar", - "test1": "test2"}) + "test1": "test2"}, + foo="bar") # Dict element can be any order self.assertIn(mock_get.call_args[0], [("/activities/foo-bar/test1-test2/list.json",), ("/activities/test1-test2/foo-bar/list.json",)]) + self.assertEqual(mock_get.call_args[1], {"foo": "bar"}) class TestActivitiesPurge(TestActivities): @mock.patch.object(trovebox.Trovebox, 'post') @@ -135,4 +137,4 @@ class TestActivityView(TestActivities): mock_get.return_value = self._return_value(self._view_wrapper( {"data": "", "type": "invalid"})) with self.assertRaises(NotImplementedError): - self.client.activity.view(self.test_activities[0], foo="bar") + self.client.activity.view(self.test_activities[0]) diff --git a/tests/unit/test_albums.py b/tests/unit/test_albums.py index 3647f3c..3469a25 100644 --- a/tests/unit/test_albums.py +++ b/tests/unit/test_albums.py @@ -37,8 +37,8 @@ class TestAlbumsList(TestAlbums): def test_albums_list(self, mock_get): """Check that the album list is returned correctly""" mock_get.return_value = self._return_value(self.test_albums_dict) - result = self.client.albums.list() - mock_get.assert_called_with("/albums/list.json") + result = self.client.albums.list(foo="bar") + mock_get.assert_called_with("/albums/list.json", foo="bar") self.assertEqual(len(result), 2) self.assertEqual(result[0].id, "1") self.assertEqual(result[0].name, "Album 1") @@ -49,24 +49,24 @@ class TestAlbumsList(TestAlbums): def test_empty_result(self, mock_get): """Check that an empty result is transformed into an empty list """ mock_get.return_value = self._return_value("") - result = self.client.albums.list() - mock_get.assert_called_with("/albums/list.json") + result = self.client.albums.list(foo="bar") + mock_get.assert_called_with("/albums/list.json", foo="bar") self.assertEqual(result, []) @mock.patch.object(trovebox.Trovebox, 'get') def test_zero_rows(self, mock_get): """Check that totalRows=0 is transformed into an empty list """ mock_get.return_value = self._return_value([{"totalRows": 0}]) - result = self.client.albums.list() - mock_get.assert_called_with("/albums/list.json") + result = self.client.albums.list(foo="bar") + mock_get.assert_called_with("/albums/list.json", foo="bar") self.assertEqual(result, []) @mock.patch.object(trovebox.Trovebox, 'get') def test_albums_list_returns_cover_photos(self, mock_get): """Check that the album list returns cover photo objects""" mock_get.return_value = self._return_value(self.test_albums_dict) - result = self.client.albums.list() - mock_get.assert_called_with("/albums/list.json") + result = self.client.albums.list(foo="bar") + mock_get.assert_called_with("/albums/list.json", foo="bar") self.assertEqual(len(result), 2) self.assertEqual(result[0].id, "1") self.assertEqual(result[0].name, "Album 1") @@ -83,7 +83,8 @@ class TestAlbumUpdateCover(TestAlbums): """Check that an album cover can be updated""" mock_post.return_value = self._return_value(self.test_albums_dict[1]) result = self.client.album.cover_update(self.test_albums[0], - self.test_photos[0], foo="bar") + self.test_photos[0], + foo="bar") mock_post.assert_called_with("/album/1/cover/1a/update.json", foo="bar") self.assertEqual(result.id, "2") @@ -134,16 +135,16 @@ class TestAlbumDelete(TestAlbums): def test_album_delete(self, mock_post): """Check that an album can be deleted""" mock_post.return_value = self._return_value(True) - result = self.client.album.delete(self.test_albums[0]) - mock_post.assert_called_with("/album/1/delete.json") + result = self.client.album.delete(self.test_albums[0], foo="bar") + mock_post.assert_called_with("/album/1/delete.json", foo="bar") self.assertEqual(result, True) @mock.patch.object(trovebox.Trovebox, 'post') def test_album_delete_id(self, mock_post): """Check that an album can be deleted using its ID""" mock_post.return_value = self._return_value(True) - result = self.client.album.delete("1") - mock_post.assert_called_with("/album/1/delete.json") + result = self.client.album.delete("1", foo="bar") + mock_post.assert_called_with("/album/1/delete.json", foo="bar") self.assertEqual(result, True) @mock.patch.object(trovebox.Trovebox, 'post') @@ -158,8 +159,8 @@ class TestAlbumDelete(TestAlbums): """Check that an album can be deleted using the album object directly""" mock_post.return_value = self._return_value(True) album = self.test_albums[0] - result = album.delete() - mock_post.assert_called_with("/album/1/delete.json") + result = album.delete(foo="bar") + mock_post.assert_called_with("/album/1/delete.json", foo="bar") self.assertEqual(result, True) self.assertEqual(album.get_fields(), {}) self.assertEqual(album.id, None) @@ -180,21 +181,23 @@ class TestAlbumAdd(TestAlbums): def test_album_add(self, mock_post): """ Check that photos can be added to an album """ mock_post.return_value = self._return_value(self.test_albums_dict[1]) - self.client.album.add(self.test_albums[0], self.test_photos, - foo="bar") + result = self.client.album.add(self.test_albums[0], self.test_photos, + foo="bar") mock_post.assert_called_with("/album/1/photo/add.json", ids=["1a", "2b"], foo="bar") + self.assertEqual(result.id, self.test_albums[1].id) @mock.patch.object(trovebox.Trovebox, 'post') def test_album_add_id(self, mock_post): """ Check that photos can be added to an album using IDs """ mock_post.return_value = self._return_value(self.test_albums_dict[1]) - self.client.album.add(self.test_albums[0].id, - objects=["1a", "2b"], - object_type="photo", - foo="bar") + result = self.client.album.add(self.test_albums[0].id, + objects=["1a", "2b"], + object_type="photo", + foo="bar") mock_post.assert_called_with("/album/1/photo/add.json", ids=["1a", "2b"], foo="bar") + self.assertEqual(result.id, self.test_albums[1].id) @mock.patch.object(trovebox.Trovebox, 'post') def test_album_object_add(self, mock_post): @@ -203,9 +206,11 @@ class TestAlbumAdd(TestAlbums): album object directly """ mock_post.return_value = self._return_value(self.test_albums_dict[1]) - self.test_albums[0].add(self.test_photos, foo="bar") + album = self.test_albums[0] + album.add(self.test_photos, foo="bar") mock_post.assert_called_with("/album/1/photo/add.json", ids=["1a", "2b"], foo="bar") + self.assertEqual(album.id, self.test_albums[1].id) @mock.patch.object(trovebox.Trovebox, 'post') def test_album_add_single(self, mock_post): @@ -238,21 +243,23 @@ class TestAlbumRemovePhotos(TestAlbums): def test_album_remove(self, mock_post): """ Check that photos can be removed from an album """ mock_post.return_value = self._return_value(self.test_albums_dict[1]) - self.client.album.remove(self.test_albums[0], self.test_photos, - foo="bar") + result = self.client.album.remove(self.test_albums[0], self.test_photos, + foo="bar") mock_post.assert_called_with("/album/1/photo/remove.json", ids=["1a", "2b"], foo="bar") + self.assertEqual(result.id, self.test_albums[1].id) @mock.patch.object(trovebox.Trovebox, 'post') def test_album_remove_id(self, mock_post): """ Check that photos can be removed from an album using IDs """ mock_post.return_value = self._return_value(self.test_albums_dict[1]) - self.client.album.remove(self.test_albums[0].id, - objects=["1a", "2b"], - object_type="photo", - foo="bar") + result = self.client.album.remove(self.test_albums[0].id, + objects=["1a", "2b"], + object_type="photo", + foo="bar") mock_post.assert_called_with("/album/1/photo/remove.json", ids=["1a", "2b"], foo="bar") + self.assertEqual(result.id, self.test_albums[1].id) @mock.patch.object(trovebox.Trovebox, 'post') def test_album_object_remove(self, mock_post): @@ -261,9 +268,11 @@ class TestAlbumRemovePhotos(TestAlbums): album object directly """ mock_post.return_value = self._return_value(self.test_albums_dict[1]) - self.test_albums[0].remove(self.test_photos, foo="bar") + album = self.test_albums[0] + album.remove(self.test_photos, foo="bar") mock_post.assert_called_with("/album/1/photo/remove.json", ids=["1a", "2b"], foo="bar") + self.assertEqual(album.id, self.test_albums[1].id) @mock.patch.object(trovebox.Trovebox, 'post') def test_album_remove_single(self, mock_post): diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index e5fb477..d549f12 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -31,8 +31,8 @@ class TestPhotosList(TestPhotos): """Check that the photo list is returned correctly""" mock_get.return_value = self._return_value(self.test_photos_dict) - result = self.client.photos.list() - mock_get.assert_called_with("/photos/list.json") + result = self.client.photos.list(foo="bar") + mock_get.assert_called_with("/photos/list.json", foo="bar") self.assertEqual(len(result), 2) self.assertEqual(result[0].id, "1a") self.assertEqual(result[0].tags, ["tag1", "tag2"]) @@ -43,16 +43,16 @@ class TestPhotosList(TestPhotos): def test_empty_result(self, mock_get): """Check that an empty result is transformed into an empty list """ mock_get.return_value = self._return_value("") - result = self.client.photos.list() - mock_get.assert_called_with("/photos/list.json") + result = self.client.photos.list(foo="bar") + mock_get.assert_called_with("/photos/list.json", foo="bar") self.assertEqual(result, []) @mock.patch.object(trovebox.Trovebox, 'get') def test_zero_rows(self, mock_get): """Check that totalRows=0 is transformed into an empty list """ mock_get.return_value = self._return_value([{"totalRows": 0}]) - result = self.client.photos.list() - mock_get.assert_called_with("/photos/list.json") + result = self.client.photos.list(foo="bar") + mock_get.assert_called_with("/photos/list.json", foo="bar") self.assertEqual(result, []) class TestPhotosUpdate(TestPhotos): @@ -89,16 +89,18 @@ class TestPhotosDelete(TestPhotos): def test_photos_delete(self, mock_post): """Check that multiple photos can be deleted""" mock_post.return_value = self._return_value(True) - result = self.client.photos.delete(self.test_photos) - mock_post.assert_called_with("/photos/delete.json", ids=["1a", "2b"]) + result = self.client.photos.delete(self.test_photos, foo="bar") + mock_post.assert_called_with("/photos/delete.json", + ids=["1a", "2b"], foo="bar") self.assertEqual(result, True) @mock.patch.object(trovebox.Trovebox, 'post') def test_photos_delete_ids(self, mock_post): """Check that multiple photos can be deleted using their IDs""" mock_post.return_value = self._return_value(True) - result = self.client.photos.delete(["1a", "2b"]) - mock_post.assert_called_with("/photos/delete.json", ids=["1a", "2b"]) + result = self.client.photos.delete(["1a", "2b"], foo="bar") + mock_post.assert_called_with("/photos/delete.json", + ids=["1a", "2b"], foo="bar") self.assertEqual(result, True) @mock.patch.object(trovebox.Trovebox, 'post') @@ -116,16 +118,16 @@ class TestPhotoDelete(TestPhotos): def test_photo_delete(self, mock_post): """Check that a photo can be deleted""" mock_post.return_value = self._return_value(True) - result = self.client.photo.delete(self.test_photos[0]) - mock_post.assert_called_with("/photo/1a/delete.json") + result = self.client.photo.delete(self.test_photos[0], foo="bar") + mock_post.assert_called_with("/photo/1a/delete.json", foo="bar") self.assertEqual(result, True) @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_delete_id(self, mock_post): """Check that a photo can be deleted using its ID""" mock_post.return_value = self._return_value(True) - result = self.client.photo.delete("1a") - mock_post.assert_called_with("/photo/1a/delete.json") + result = self.client.photo.delete("1a", foo="bar") + mock_post.assert_called_with("/photo/1a/delete.json", foo="bar") self.assertEqual(result, True) @mock.patch.object(trovebox.Trovebox, 'post') @@ -143,8 +145,8 @@ class TestPhotoDelete(TestPhotos): """ mock_post.return_value = self._return_value(True) photo = self.test_photos[0] - result = photo.delete() - mock_post.assert_called_with("/photo/1a/delete.json") + result = photo.delete(foo="bar") + mock_post.assert_called_with("/photo/1a/delete.json", foo="bar") self.assertEqual(result, True) self.assertEqual(photo.get_fields(), {}) self.assertEqual(photo.id, None) @@ -309,8 +311,10 @@ class TestPhotoNextPrevious(TestPhotos): mock_get.return_value = self._return_value( {"next": [self.test_photos_dict[0]], "previous": [self.test_photos_dict[1]]}) - result = self.client.photo.next_previous(self.test_photos[0]) - mock_get.assert_called_with("/photo/1a/nextprevious.json") + result = self.client.photo.next_previous(self.test_photos[0], + foo="bar") + mock_get.assert_called_with("/photo/1a/nextprevious.json", + foo="bar") self.assertEqual(result["next"][0].get_fields(), self.test_photos_dict[0]) self.assertEqual(result["previous"][0].get_fields(), @@ -325,8 +329,9 @@ class TestPhotoNextPrevious(TestPhotos): mock_get.return_value = self._return_value( {"next": [self.test_photos_dict[0]], "previous": [self.test_photos_dict[1]]}) - result = self.client.photo.next_previous("1a") - mock_get.assert_called_with("/photo/1a/nextprevious.json") + result = self.client.photo.next_previous("1a", foo="bar") + mock_get.assert_called_with("/photo/1a/nextprevious.json", + foo="bar") self.assertEqual(result["next"][0].get_fields(), self.test_photos_dict[0]) self.assertEqual(result["previous"][0].get_fields(), @@ -341,8 +346,9 @@ class TestPhotoNextPrevious(TestPhotos): mock_get.return_value = self._return_value( {"next": [self.test_photos_dict[0]], "previous": [self.test_photos_dict[1]]}) - result = self.test_photos[0].next_previous() - mock_get.assert_called_with("/photo/1a/nextprevious.json") + result = self.test_photos[0].next_previous(foo="bar") + mock_get.assert_called_with("/photo/1a/nextprevious.json", + foo="bar") self.assertEqual(result["next"][0].get_fields(), self.test_photos_dict[0]) self.assertEqual(result["previous"][0].get_fields(), @@ -353,8 +359,10 @@ class TestPhotoNextPrevious(TestPhotos): """Check that the next photos are returned""" mock_get.return_value = self._return_value( {"next": [self.test_photos_dict[0]]}) - result = self.client.photo.next_previous(self.test_photos[0]) - mock_get.assert_called_with("/photo/1a/nextprevious.json") + result = self.client.photo.next_previous(self.test_photos[0], + foo="bar") + mock_get.assert_called_with("/photo/1a/nextprevious.json", + foo="bar") self.assertEqual(result["next"][0].get_fields(), self.test_photos_dict[0]) self.assertNotIn("previous", result) @@ -364,8 +372,10 @@ class TestPhotoNextPrevious(TestPhotos): """Check that the previous photos are returned""" mock_get.return_value = self._return_value( {"previous": [self.test_photos_dict[1]]}) - result = self.client.photo.next_previous(self.test_photos[0]) - mock_get.assert_called_with("/photo/1a/nextprevious.json") + result = self.client.photo.next_previous(self.test_photos[0], + foo="bar") + mock_get.assert_called_with("/photo/1a/nextprevious.json", + foo="bar") self.assertEqual(result["previous"][0].get_fields(), self.test_photos_dict[1]) self.assertNotIn("next", result) @@ -376,8 +386,10 @@ class TestPhotoNextPrevious(TestPhotos): mock_get.return_value = self._return_value( {"next": [self.test_photos_dict[0], self.test_photos_dict[0]], "previous": [self.test_photos_dict[1], self.test_photos_dict[1]]}) - result = self.client.photo.next_previous(self.test_photos[0]) - mock_get.assert_called_with("/photo/1a/nextprevious.json") + result = self.client.photo.next_previous(self.test_photos[0], + foo="bar") + mock_get.assert_called_with("/photo/1a/nextprevious.json", + foo="bar") self.assertEqual(result["next"][0].get_fields(), self.test_photos_dict[0]) self.assertEqual(result["next"][1].get_fields(), diff --git a/tests/unit/test_tags.py b/tests/unit/test_tags.py index fd5b88a..47a71e4 100644 --- a/tests/unit/test_tags.py +++ b/tests/unit/test_tags.py @@ -27,8 +27,8 @@ class TestTagsList(TestTags): def test_tags_list(self, mock_get): """Check that the tag list is returned correctly""" mock_get.return_value = self._return_value(self.test_tags_dict) - result = self.client.tags.list() - mock_get.assert_called_with("/tags/list.json") + result = self.client.tags.list(foo="bar") + mock_get.assert_called_with("/tags/list.json", foo="bar") self.assertEqual(len(result), 2) self.assertEqual(result[0].id, "tag1") self.assertEqual(result[0].count, 11) @@ -39,16 +39,16 @@ class TestTagsList(TestTags): def test_empty_result(self, mock_get): """Check that an empty result is transformed into an empty list """ mock_get.return_value = self._return_value("") - result = self.client.tags.list() - mock_get.assert_called_with("/tags/list.json") + result = self.client.tags.list(foo="bar") + mock_get.assert_called_with("/tags/list.json", foo="bar") self.assertEqual(result, []) @mock.patch.object(trovebox.Trovebox, 'get') def test_zero_rows(self, mock_get): """Check that totalRows=0 is transformed into an empty list """ mock_get.return_value = self._return_value([{"totalRows": 0}]) - result = self.client.tags.list() - mock_get.assert_called_with("/tags/list.json") + result = self.client.tags.list(foo="bar") + mock_get.assert_called_with("/tags/list.json", foo="bar") self.assertEqual(result, []) class TestTagCreate(TestTags): @@ -56,8 +56,9 @@ class TestTagCreate(TestTags): def test_tag_create(self, mock_post): """Check that a tag can be created""" mock_post.return_value = self._return_value(True) - result = self.client.tag.create("test") - mock_post.assert_called_with("/tag/create.json", tag="test") + result = self.client.tag.create("test", foo="bar") + mock_post.assert_called_with("/tag/create.json", tag="test", + foo="bar") self.assertEqual(result, True) class TestTagDelete(TestTags): @@ -65,16 +66,16 @@ class TestTagDelete(TestTags): def test_tag_delete(self, mock_post): """Check that a tag can be deleted""" mock_post.return_value = self._return_value(True) - result = self.client.tag.delete(self.test_tags[0]) - mock_post.assert_called_with("/tag/tag1/delete.json") + result = self.client.tag.delete(self.test_tags[0], foo="bar") + mock_post.assert_called_with("/tag/tag1/delete.json", foo="bar") self.assertEqual(result, True) @mock.patch.object(trovebox.Trovebox, 'post') def test_tag_delete_id(self, mock_post): """Check that a tag can be deleted using its ID""" mock_post.return_value = self._return_value(True) - result = self.client.tag.delete("tag1") - mock_post.assert_called_with("/tag/tag1/delete.json") + result = self.client.tag.delete("tag1", foo="bar") + mock_post.assert_called_with("/tag/tag1/delete.json", foo="bar") self.assertEqual(result, True) @mock.patch.object(trovebox.Trovebox, 'post') @@ -89,8 +90,8 @@ class TestTagDelete(TestTags): """Check that a tag can be deleted when using the tag object directly""" mock_post.return_value = self._return_value(True) tag = self.test_tags[0] - result = tag.delete() - mock_post.assert_called_with("/tag/tag1/delete.json") + result = tag.delete(foo="bar") + mock_post.assert_called_with("/tag/tag1/delete.json", foo="bar") self.assertEqual(result, True) self.assertEqual(tag.get_fields(), {}) self.assertEqual(tag.id, None) diff --git a/trovebox/.pylint-ignores.patch b/trovebox/.pylint-ignores.patch index 24996b7..af3e9da 100644 --- a/trovebox/.pylint-ignores.patch +++ b/trovebox/.pylint-ignores.patch @@ -14,7 +14,7 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_al --- original/api/api_album.py +++ patched/api/api_album.py @@ -8,7 +8,7 @@ - from trovebox import http + from trovebox.objects.album import Album from .api_base import ApiBase -class ApiAlbums(ApiBase): @@ -37,7 +37,16 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_ba diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_tag.py patched/api/api_tag.py --- original/api/api_tag.py +++ patched/api/api_tag.py -@@ -5,7 +5,7 @@ +@@ -2,7 +2,7 @@ + api_tag.py : Trovebox Tag API Classes + """ + try: +- from urllib.parse import quote # Python3 ++ from urllib.parse import quote # Python3 # pylint: disable=import-error,no-name-in-module + except ImportError: + from urllib import quote # Python2 + +@@ -10,7 +10,7 @@ from trovebox.objects.tag import Tag from .api_base import ApiBase @@ -159,27 +168,15 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/main.py pa files=files, **params) for file_ in files: files[file_].close() -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/tag.py patched/objects/tag.py ---- original/objects/tag.py -+++ patched/objects/tag.py -@@ -2,7 +2,7 @@ - Representation of a Tag object - """ - try: -- from urllib.parse import quote # Python3 -+ from urllib.parse import quote # Python3 # pylint: disable=import-error,no-name-in-module - except ImportError: - from urllib import quote # Python2 - diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/trovebox_object.py patched/objects/trovebox_object.py --- original/objects/trovebox_object.py +++ patched/objects/trovebox_object.py @@ -5,7 +5,7 @@ """ Base object supporting the storage of custom fields as attributes """ - def __init__(self, trovebox, json_dict): - self._type = "None" + _type = "None" + def __init__(self, client, json_dict): - self.id = None + self.id = None # pylint: disable=invalid-name self.name = None - self._trovebox = trovebox + self._client = client self._json_dict = json_dict diff --git a/trovebox/api/api_action.py b/trovebox/api/api_action.py index 9ffed5f..a1dda45 100644 --- a/trovebox/api/api_action.py +++ b/trovebox/api/api_action.py @@ -1,6 +1,7 @@ """ api_action.py : Trovebox Action API Classes """ +from trovebox.errors import TroveboxError from trovebox.objects.action import Action from .api_base import ApiBase @@ -15,15 +16,14 @@ class ApiAction(ApiBase): If a Trovebox object is used, the target type is inferred automatically. """ - # Extract the type from the target + # Extract the target type if target_type is None: target_type = target.get_type() - # Extract the ID from the target + # Extract the target ID try: target_id = target.id except AttributeError: - # Assume the ID was passed in directly target_id = target result = self._client.post("/action/%s/%s/create.json" % @@ -39,9 +39,12 @@ class ApiAction(ApiBase): Returns True if successful. Raises a TroveboxError if not. """ - if not isinstance(action, Action): - action = Action(self._client, {"id": action}) - return action.delete(**kwds) + result = self._client.post("/action/%s/delete.json" % + self._extract_id(action), + **kwds)["result"] + if not result: + raise TroveboxError("Delete response returned False") + return result def view(self, action, **kwds): """ @@ -50,7 +53,7 @@ class ApiAction(ApiBase): Requests all properties of an action. Returns the requested action object. """ - if not isinstance(action, Action): - action = Action(self._client, {"id": action}) - action.view(**kwds) - return action + result = self._client.get("/action/%s/view.json" % + self._extract_id(action), + **kwds)["result"] + return Action(self._client, result) diff --git a/trovebox/api/api_activity.py b/trovebox/api/api_activity.py index 0f3bcea..bf2e63e 100644 --- a/trovebox/api/api_activity.py +++ b/trovebox/api/api_activity.py @@ -1,7 +1,7 @@ """ api_activity.py : Trovebox Activity API Classes """ -from trovebox import http +import json from trovebox.errors import TroveboxError from trovebox.objects.activity import Activity from .api_base import ApiBase @@ -19,7 +19,7 @@ class ApiActivities(ApiBase): filter_string = self._build_filter_string(filters) activities = self._client.get("/activities/%slist.json" % filter_string, **kwds)["result"] - activities = http.result_to_list(activities) + activities = self._result_to_list(activities) return [Activity(self._client, activity) for activity in activities] def purge(self, **kwds): @@ -42,7 +42,11 @@ class ApiActivity(ApiBase): Requests all properties of an activity. Returns the requested activity object. """ - if not isinstance(activity, Activity): - activity = Activity(self._client, {"id": activity}) - activity.view(**kwds) - return activity + result = self._client.get("/activity/%s/view.json" % + self._extract_id(activity), + **kwds)["result"] + + # TBD: Why is the result enclosed/encoded like this? + result = result["0"] + result["data"] = json.loads(result["data"]) + return Activity(self._client, result) diff --git a/trovebox/api/api_album.py b/trovebox/api/api_album.py index b44b9b8..87679f4 100644 --- a/trovebox/api/api_album.py +++ b/trovebox/api/api_album.py @@ -3,9 +3,9 @@ api_album.py : Trovebox Album API Classes """ import collections +from trovebox.errors import TroveboxError from trovebox.objects.trovebox_object import TroveboxObject from trovebox.objects.album import Album -from trovebox import http from .api_base import ApiBase class ApiAlbums(ApiBase): @@ -17,7 +17,7 @@ class ApiAlbums(ApiBase): Returns a list of Album objects. """ albums = self._client.get("/albums/list.json", **kwds)["result"] - albums = http.result_to_list(albums) + albums = self._result_to_list(albums) return [Album(self._client, album) for album in albums] class ApiAlbum(ApiBase): @@ -29,10 +29,18 @@ class ApiAlbum(ApiBase): Update the cover photo of an album. Returns the updated album object. """ - if not isinstance(album, Album): - album = Album(self._client, {"id": album}) - album.cover_update(photo, **kwds) - return album + result = self._client.post("/album/%s/cover/%s/update.json" % + (self._extract_id(album), + self._extract_id(photo)), + **kwds)["result"] + + # API currently doesn't return the updated album + # (frontend issue #1369) + if isinstance(result, bool): # pragma: no cover + result = self._client.get("/album/%s/view.json" % + self._extract_id(album))["result"] + + return Album(self._client, result) def create(self, name, **kwds): """ @@ -52,9 +60,12 @@ class ApiAlbum(ApiBase): Returns True if successful. Raises a TroveboxError if not. """ - if not isinstance(album, Album): - album = Album(self._client, {"id": album}) - return album.delete(**kwds) + result = self._client.post("/album/%s/delete.json" % + self._extract_id(album), + **kwds)["result"] + if not result: + raise TroveboxError("Delete response returned False") + return result def add(self, album, objects, object_type=None, **kwds): """ @@ -64,7 +75,7 @@ class ApiAlbum(ApiBase): The objects are a list of either IDs or Trovebox objects. If Trovebox objects are used, the object type is inferred automatically. - Returns True if the album was updated successfully. + Returns the updated album object. """ return self._add_remove("add", album, objects, object_type, **kwds) @@ -77,7 +88,7 @@ class ApiAlbum(ApiBase): The objects are a list of either IDs or Trovebox objects. If Trovebox objects are used, the object type is inferred automatically. - Returns True if the album was updated successfully. + Returns the updated album object. """ return self._add_remove("remove", album, objects, object_type, **kwds) @@ -85,10 +96,6 @@ class ApiAlbum(ApiBase): def _add_remove(self, action, album, objects, object_type=None, **kwds): """Common code for the add and remove endpoints.""" - # Extract the id of the album - if isinstance(album, Album): - album = album.id - # Ensure we have an iterable of objects if not isinstance(objects, collections.Iterable): objects = [objects] @@ -106,9 +113,17 @@ class ApiAlbum(ApiBase): # Extract the ids of the objects objects[i] = obj.id - return self._client.post("/album/%s/%s/%s.json" % - (album, object_type, action), - ids=objects, **kwds)["result"] + result = self._client.post("/album/%s/%s/%s.json" % + (self._extract_id(album), + object_type, action), + ids=objects, **kwds)["result"] + + # API currently doesn't return the updated album + # (frontend issue #1369) + if isinstance(result, bool): # pragma: no cover + result = self._client.get("/album/%s/view.json" % + self._extract_id(album))["result"] + return Album(self._client, result) def update(self, album, **kwds): """ @@ -117,10 +132,16 @@ class ApiAlbum(ApiBase): Updates an album with the specified parameters. Returns the updated album object. """ - if not isinstance(album, Album): - album = Album(self._client, {"id": album}) - album.update(**kwds) - return album + result = self._client.post("/album/%s/update.json" % + self._extract_id(album), + **kwds)["result"] + + # APIv1 doesn't return the updated album (frontend issue #937) + if isinstance(result, bool): # pragma: no cover + result = self._client.get("/album/%s/view.json" % + self._extract_id(album))["result"] + + return Album(self._client, result) def view(self, album, **kwds): """ @@ -129,7 +150,7 @@ class ApiAlbum(ApiBase): Requests all properties of an album. Returns the requested album object. """ - if not isinstance(album, Album): - album = Album(self._client, {"id": album}) - album.view(**kwds) - return album + result = self._client.get("/album/%s/view.json" % + self._extract_id(album), + **kwds)["result"] + return Album(self._client, result) diff --git a/trovebox/api/api_base.py b/trovebox/api/api_base.py index 553a4c7..15f0f4b 100644 --- a/trovebox/api/api_base.py +++ b/trovebox/api/api_base.py @@ -18,3 +18,21 @@ class ApiBase(object): for filt in filters: filter_string += "%s-%s/" % (filt, filters[filt]) return filter_string + + @staticmethod + def _extract_id(obj): + """ Return obj.id, or obj if the object doesn't have an ID """ + try: + return obj.id + except AttributeError: + return obj + + @staticmethod + def _result_to_list(result): + """ Handle the case where the result contains no items """ + if not result: + return [] + if "totalRows" in result[0] and result[0]["totalRows"] == 0: + return [] + else: + return result diff --git a/trovebox/api/api_photo.py b/trovebox/api/api_photo.py index eca7f0c..edf5338 100644 --- a/trovebox/api/api_photo.py +++ b/trovebox/api/api_photo.py @@ -3,24 +3,10 @@ api_photo.py : Trovebox Photo API Classes """ import base64 -from trovebox import http from trovebox.errors import TroveboxError from trovebox.objects.photo import Photo from .api_base import ApiBase -def _extract_ids(photos): - """ - Given a list of objects, extract the photo id for each Photo - object. - """ - ids = [] - for photo in photos: - if isinstance(photo, Photo): - ids.append(photo.id) - else: - ids.append(photo) - return ids - class ApiPhotos(ApiBase): """ Definitions of /photos/ API endpoints """ # TODO: Add options @@ -31,7 +17,7 @@ class ApiPhotos(ApiBase): Returns a list of Photo objects. """ photos = self._client.get("/photos/list.json", **kwds)["result"] - photos = http.result_to_list(photos) + photos = self._result_to_list(photos) return [Photo(self._client, photo) for photo in photos] # def share(self, **kwds): @@ -44,7 +30,7 @@ class ApiPhotos(ApiBase): Returns True if successful. Raises a TroveboxError if not. """ - ids = _extract_ids(photos) + ids = [self._extract_id(photo) for photo in photos] if not self._client.post("/photos/delete.json", ids=ids, **kwds)["result"]: raise TroveboxError("Delete response returned False") @@ -58,7 +44,7 @@ class ApiPhotos(ApiBase): Returns True if successful. Raises TroveboxError if not. """ - ids = _extract_ids(photos) + ids = [self._extract_id(photo) for photo in photos] if not self._client.post("/photos/update.json", ids=ids, **kwds)["result"]: raise TroveboxError("Update response returned False") @@ -74,9 +60,12 @@ class ApiPhoto(ApiBase): Returns True if successful. Raises a TroveboxError if not. """ - if not isinstance(photo, Photo): - photo = Photo(self._client, {"id": photo}) - return photo.delete(**kwds) + result = self._client.post("/photo/%s/delete.json" % + self._extract_id(photo), + **kwds)["result"] + if not result: + raise TroveboxError("Delete response returned False") + return result # def delete_source(self, photo, **kwds): @@ -95,10 +84,10 @@ class ApiPhoto(ApiBase): Updates a photo with the specified parameters. Returns the updated photo object. """ - if not isinstance(photo, Photo): - photo = Photo(self._client, {"id": photo}) - photo.update(**kwds) - return photo + result = self._client.post("/photo/%s/update.json" % + self._extract_id(photo), + **kwds)["result"] + return Photo(self._client, result) # TODO: Add options def view(self, photo, **kwds): @@ -110,10 +99,10 @@ class ApiPhoto(ApiBase): by using the "returnSizes" parameter. Returns the requested photo object. """ - if not isinstance(photo, Photo): - photo = Photo(self._client, {"id": photo}) - photo.view(**kwds) - return photo + result = self._client.get("/photo/%s/view.json" % + self._extract_id(photo), + **kwds)["result"] + return Photo(self._client, result) def upload(self, photo_file, **kwds): """ @@ -151,9 +140,29 @@ class ApiPhoto(ApiBase): Returns a dict containing the next and previous photo lists (there may be more than one next/previous photo returned). """ - if not isinstance(photo, Photo): - photo = Photo(self._client, {"id": photo}) - return photo.next_previous(**kwds) + result = self._client.get("/photo/%s/nextprevious.json" % + self._extract_id(photo), + **kwds)["result"] + value = {} + if "next" in result: + # Workaround for APIv1 + if not isinstance(result["next"], list): # pragma: no cover + result["next"] = [result["next"]] + + value["next"] = [] + for photo in result["next"]: + value["next"].append(Photo(self._client, photo)) + + if "previous" in result: + # Workaround for APIv1 + if not isinstance(result["previous"], list): # pragma: no cover + result["previous"] = [result["previous"]] + + value["previous"] = [] + for photo in result["previous"]: + value["previous"].append(Photo(self._client, photo)) + + return value def transform(self, photo, **kwds): """ @@ -163,7 +172,13 @@ class ApiPhoto(ApiBase): eg. transform(photo, rotate=90) Returns the transformed photo. """ - if not isinstance(photo, Photo): - photo = Photo(self._client, {"id": photo}) - photo.transform(**kwds) - return photo + result = self._client.post("/photo/%s/transform.json" % + self._extract_id(photo), + **kwds)["result"] + + # APIv1 doesn't return the transformed photo (frontend issue #955) + if isinstance(result, bool): # pragma: no cover + result = self._client.get("/photo/%s/view.json" % + self._extract_id(photo))["result"] + + return Photo(self._client, result) diff --git a/trovebox/api/api_tag.py b/trovebox/api/api_tag.py index fbab095..6f8e513 100644 --- a/trovebox/api/api_tag.py +++ b/trovebox/api/api_tag.py @@ -1,7 +1,12 @@ """ api_tag.py : Trovebox Tag API Classes """ -from trovebox import http +try: + from urllib.parse import quote # Python3 +except ImportError: + from urllib import quote # Python2 + +from trovebox.errors import TroveboxError from trovebox.objects.tag import Tag from .api_base import ApiBase @@ -14,7 +19,7 @@ class ApiTags(ApiBase): Returns a list of Tag objects. """ tags = self._client.get("/tags/list.json", **kwds)["result"] - tags = http.result_to_list(tags) + tags = self._result_to_list(tags) return [Tag(self._client, tag) for tag in tags] class ApiTag(ApiBase): @@ -37,9 +42,12 @@ class ApiTag(ApiBase): Returns True if successful. Raises a TroveboxError if not. """ - if not isinstance(tag, Tag): - tag = Tag(self._client, {"id": tag}) - return tag.delete(**kwds) + result = self._client.post("/tag/%s/delete.json" % + quote(self._extract_id(tag)), + **kwds)["result"] + if not result: + raise TroveboxError("Delete response returned False") + return result def update(self, tag, **kwds): """ @@ -48,9 +56,9 @@ class ApiTag(ApiBase): Updates a tag with the specified parameters. Returns the updated tag object. """ - if not isinstance(tag, Tag): - tag = Tag(self._client, {"id": tag}) - tag.update(**kwds) - return tag + result = self._client.post("/tag/%s/update.json" % + quote(self._extract_id(tag)), + **kwds)["result"] + return Tag(self._client, result) # def view(self, tag, **kwds): diff --git a/trovebox/http.py b/trovebox/http.py index 30e342e..21e4c69 100644 --- a/trovebox/http.py +++ b/trovebox/http.py @@ -249,12 +249,3 @@ class Http(object): raise TroveboxDuplicateError("Code %d: %s" % (code, message)) else: raise TroveboxError("Code %d: %s" % (code, message)) - -def result_to_list(result): - """ Handle the case where the result contains no items """ - if not result: - return [] - if "totalRows" in result[0] and result[0]["totalRows"] == 0: - return [] - else: - return result diff --git a/trovebox/objects/action.py b/trovebox/objects/action.py index 847a12c..955c238 100644 --- a/trovebox/objects/action.py +++ b/trovebox/objects/action.py @@ -1,7 +1,6 @@ """ Representation of an Action object """ -from trovebox.errors import TroveboxError from .trovebox_object import TroveboxObject from .photo import Photo @@ -33,10 +32,7 @@ class Action(TroveboxObject): Returns True if successful. Raises a TroveboxError if not. """ - result = self._client.post("/action/%s/delete.json" % - self.id, **kwds)["result"] - if not result: - raise TroveboxError("Delete response returned False") + result = self._client.action.delete(self, **kwds) self._delete_fields() return result @@ -45,9 +41,8 @@ class Action(TroveboxObject): Endpoint: /action//view.json Requests the full contents of the action. - Updates the action's fields with the response. + Updates the action object's fields with the response. """ - result = self._client.get("/action/%s/view.json" % - self.id, **kwds)["result"] - self._replace_fields(result) + result = self._client.action.view(self, **kwds) + self._replace_fields(result.get_fields()) self._update_fields_with_objects() diff --git a/trovebox/objects/activity.py b/trovebox/objects/activity.py index 1aeffc2..e95c2da 100644 --- a/trovebox/objects/activity.py +++ b/trovebox/objects/activity.py @@ -1,8 +1,6 @@ """ Representation of an Activity object """ -import json - from .trovebox_object import TroveboxObject from .photo import Photo @@ -33,12 +31,7 @@ class Activity(TroveboxObject): Requests the full contents of the activity. Updates the activity's fields with the response. """ - result = self._client.get("/activity/%s/view.json" % - self.id, **kwds)["result"] - - # TBD: Why is the result enclosed/encoded like this? - result = result["0"] - result["data"] = json.loads(result["data"]) - - self._replace_fields(result) + result = self._client.activity.view(self, **kwds) + self._replace_fields(result.get_fields()) self._update_fields_with_objects() + diff --git a/trovebox/objects/album.py b/trovebox/objects/album.py index 4a65cc5..352891e 100644 --- a/trovebox/objects/album.py +++ b/trovebox/objects/album.py @@ -1,7 +1,6 @@ """ Representation of an Album object """ -from trovebox.errors import TroveboxError from .trovebox_object import TroveboxObject from .photo import Photo @@ -38,19 +37,8 @@ class Album(TroveboxObject): Update the cover photo of this album. """ - if not isinstance(photo, Photo): - photo = Photo(self._client, {"id": photo}) - - result = self._client.post("/album/%s/cover/%s/update.json" % - (self.id, photo.id), - **kwds)["result"] - - # API currently doesn't return the updated album - # (frontend issue #1369) - if isinstance(result, bool): # pragma: no cover - result = self._client.get("/album/%s/view.json" % - self.id)["result"] - self._replace_fields(result) + result = self._client.album.cover_update(self, photo, **kwds) + self._replace_fields(result.get_fields()) self._update_fields_with_objects() def delete(self, **kwds): @@ -61,10 +49,7 @@ class Album(TroveboxObject): Returns True if successful. Raises a TroveboxError if not. """ - result = self._client.post("/album/%s/delete.json" % - self.id, **kwds)["result"] - if not result: - raise TroveboxError("Delete response returned False") + result = self._client.album.delete(self, **kwds) self._delete_fields() return result @@ -79,13 +64,7 @@ class Album(TroveboxObject): Updates the album's fields with the response. """ result = self._client.album.add(self, objects, object_type, **kwds) - - # API currently doesn't return the updated album - # (frontend issue #1369) - if isinstance(result, bool): # pragma: no cover - result = self._client.get("/album/%s/view.json" % - self.id)["result"] - self._replace_fields(result) + self._replace_fields(result.get_fields()) self._update_fields_with_objects() def remove(self, objects, object_type=None, **kwds): @@ -100,12 +79,7 @@ class Album(TroveboxObject): """ result = self._client.album.remove(self, objects, object_type, **kwds) - # API currently doesn't return the updated album - # (frontend issue #1369) - if isinstance(result, bool): # pragma: no cover - result = self._client.get("/album/%s/view.json" % - self.id)["result"] - self._replace_fields(result) + self._replace_fields(result.get_fields()) self._update_fields_with_objects() def update(self, **kwds): @@ -114,15 +88,8 @@ class Album(TroveboxObject): Updates this album with the specified parameters. """ - result = self._client.post("/album/%s/update.json" % - self.id, **kwds)["result"] - - # APIv1 doesn't return the updated album (frontend issue #937) - if isinstance(result, bool): # pragma: no cover - result = self._client.get("/album/%s/view.json" % - self.id)["result"] - - self._replace_fields(result) + result = self._client.album.update(self, **kwds) + self._replace_fields(result.get_fields()) self._update_fields_with_objects() def view(self, **kwds): @@ -132,7 +99,6 @@ class Album(TroveboxObject): Requests all properties of an album. Updates the album's fields with the response. """ - result = self._client.get("/album/%s/view.json" % - self.id, **kwds)["result"] - self._replace_fields(result) + result = self._client.album.view(self, **kwds) + self._replace_fields(result.get_fields()) self._update_fields_with_objects() diff --git a/trovebox/objects/photo.py b/trovebox/objects/photo.py index 2b6fc5c..28865c5 100644 --- a/trovebox/objects/photo.py +++ b/trovebox/objects/photo.py @@ -1,7 +1,6 @@ """ Representation of a Photo object """ -from trovebox.errors import TroveboxError from .trovebox_object import TroveboxObject class Photo(TroveboxObject): @@ -16,10 +15,7 @@ class Photo(TroveboxObject): Returns True if successful. Raises a TroveboxError if not. """ - result = self._client.post("/photo/%s/delete.json" % - self.id, **kwds)["result"] - if not result: - raise TroveboxError("Delete response returned False") + result = self._client.photo.delete(self, **kwds) self._delete_fields() return result @@ -39,9 +35,8 @@ class Photo(TroveboxObject): Updates this photo with the specified parameters. """ - result = self._client.post("/photo/%s/update.json" % - self.id, **kwds)["result"] - self._replace_fields(result) + result = self._client.photo.update(self, **kwds) + self._replace_fields(result.get_fields()) # TODO: Add options def view(self, **kwds): @@ -53,9 +48,8 @@ class Photo(TroveboxObject): by using the "returnSizes" parameter. Updates the photo's fields with the response. """ - result = self._client.get("/photo/%s/view.json" % - self.id, **kwds)["result"] - self._replace_fields(result) + result = self._client.photo.view(self, **kwds) + self._replace_fields(result.get_fields()) def dynamic_url(self, **kwds): """ Not implemented yet """ @@ -69,28 +63,7 @@ class Photo(TroveboxObject): Returns a dict containing the next and previous photo lists (there may be more than one next/previous photo returned). """ - result = self._client.get("/photo/%s/nextprevious.json" % - self.id, **kwds)["result"] - value = {} - if "next" in result: - # Workaround for APIv1 - if not isinstance(result["next"], list): # pragma: no cover - result["next"] = [result["next"]] - - value["next"] = [] - for photo in result["next"]: - value["next"].append(Photo(self._client, photo)) - - if "previous" in result: - # Workaround for APIv1 - if not isinstance(result["previous"], list): # pragma: no cover - result["previous"] = [result["previous"]] - - value["previous"] = [] - for photo in result["previous"]: - value["previous"].append(Photo(self._client, photo)) - - return value + return self._client.photo.next_previous(self, **kwds) def transform(self, **kwds): """ @@ -100,12 +73,5 @@ class Photo(TroveboxObject): eg. transform(photo, rotate=90) Updates the photo's fields with the response. """ - result = self._client.post("/photo/%s/transform.json" % - self.id, **kwds)["result"] - - # APIv1 doesn't return the transformed photo (frontend issue #955) - if isinstance(result, bool): # pragma: no cover - result = self._client.get("/photo/%s/view.json" % - self.id)["result"] - - self._replace_fields(result) + result = self._client.photo.transform(self, **kwds) + self._replace_fields(result.get_fields()) diff --git a/trovebox/objects/tag.py b/trovebox/objects/tag.py index 159ec51..19326d4 100644 --- a/trovebox/objects/tag.py +++ b/trovebox/objects/tag.py @@ -1,12 +1,6 @@ """ Representation of a Tag object """ -try: - from urllib.parse import quote # Python3 -except ImportError: - from urllib import quote # Python2 - -from trovebox.errors import TroveboxError from .trovebox_object import TroveboxObject class Tag(TroveboxObject): @@ -21,10 +15,7 @@ class Tag(TroveboxObject): Returns True if successful. Raises a TroveboxError if not. """ - result = self._client.post("/tag/%s/delete.json" % - quote(self.id), **kwds)["result"] - if not result: - raise TroveboxError("Delete response returned False") + result = self._client.tag.delete(self, **kwds) self._delete_fields() return result @@ -35,8 +26,7 @@ class Tag(TroveboxObject): Updates this tag with the specified parameters. Returns the updated tag object. """ - result = self._client.post("/tag/%s/update.json" % quote(self.id), - **kwds)["result"] - self._replace_fields(result) + result = self._client.tag.update(self, **kwds) + self._replace_fields(result.get_fields()) # def view(self, **kwds): From d2e621935abb290d0cf9484f162dd67d280e022d Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Tue, 10 Sep 2013 17:52:56 +0100 Subject: [PATCH 47/80] Skip newer album tests with APIv1 --- tests/functional/test_albums.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/functional/test_albums.py b/tests/functional/test_albums.py index d6b3949..15331c7 100644 --- a/tests/functional/test_albums.py +++ b/tests/functional/test_albums.py @@ -1,3 +1,8 @@ +try: + import unittest2 as unittest # Python2.6 +except ImportError: + import unittest + from tests.functional import test_base from trovebox.objects.album import Album @@ -54,12 +59,16 @@ class TestAlbums(test_base.TestBase): self.albums = self.client.albums.list() self.assertEqual(self.albums[0].name, self.TEST_ALBUM) + @unittest.skipIf(test_base.get_test_server_api() == 1, + "update_cover was introduced in APIv2") def test_update_cover(self): """ Test that an album cover can be updated """ self.assertNotEqual(self.albums[0].cover.id, self.photos[1].id) self.albums[0].cover_update(self.photos[1]) self.assertEqual(self.albums[0].cover.id, self.photos[1].id) + @unittest.skipIf(test_base.get_test_server_api() == 1, + "includeElements was introduced in APIv2") def test_view(self): """ Test the album view """ # Do a view() with includeElements=False, using a fresh Album object From 76f27a7dbbbf9e018e313de7e6d99f99906b235e Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Tue, 10 Sep 2013 18:04:49 +0100 Subject: [PATCH 48/80] Try to put the test environment back the way we found it, to save teardown/setup time. --- tests/functional/test_activities.py | 8 ++++++++ tests/functional/test_albums.py | 4 ++++ tests/functional/test_tags.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_activities.py b/tests/functional/test_activities.py index c07cc33..854e70a 100644 --- a/tests/functional/test_activities.py +++ b/tests/functional/test_activities.py @@ -26,6 +26,10 @@ class TestActivities(test_base.TestBase): for activity in activities: self.assertIn(activity.data.id, [photo.id for photo in photos]) + # Put the environment back the way we found it + for photo in photos: + photo.update(tags=self.TEST_TAG) + def test_list_filter(self): """ Check that the activity list filter parameter works correctly @@ -43,6 +47,10 @@ class TestActivities(test_base.TestBase): self.assertEqual(len(upload_activities), len(photos)) self.assertEqual(len(update_activities), 1) + # Put the environment back the way we found it + for photo in photos: + photo.update(tags=self.TEST_TAG) + # The purge endpoint currently reports a 500: Internal Server Error # PHP Fatal error: # Call to undefined method DatabaseMySql::postActivitiesPurge() diff --git a/tests/functional/test_albums.py b/tests/functional/test_albums.py index 15331c7..363c339 100644 --- a/tests/functional/test_albums.py +++ b/tests/functional/test_albums.py @@ -63,6 +63,7 @@ class TestAlbums(test_base.TestBase): "update_cover was introduced in APIv2") def test_update_cover(self): """ Test that an album cover can be updated """ + self.albums[0].cover_update(self.photos[0]) self.assertNotEqual(self.albums[0].cover.id, self.photos[1].id) self.albums[0].cover_update(self.photos[1]) self.assertEqual(self.albums[0].cover.id, self.photos[1].id) @@ -102,3 +103,6 @@ class TestAlbums(test_base.TestBase): self.assertNotIn(self.photos[0].id, [p.id for p in album.photos]) self.assertIn(self.photos[1].id, [p.id for p in album.photos]) self.assertIn(self.photos[2].id, [p.id for p in album.photos]) + + # Put the environment back the way we found it + album.add(self.photos[0]) diff --git a/tests/functional/test_tags.py b/tests/functional/test_tags.py index dc24dfc..e33e0cc 100644 --- a/tests/functional/test_tags.py +++ b/tests/functional/test_tags.py @@ -89,7 +89,7 @@ class TestTags(test_base.TestBase): # TODO: Un-skip this test once issue #919 is resolved - # tags with double-slashes cannot be deleted - @unittest.expectedFailure + @unittest.skip("Tags with double-slashed cannot be deleted") def test_tag_with_double_slashes(self): """ Run test_create_delete using a tag containing double-slashes """ self.test_create_delete("tag//with//double//slashes") From e904a629f9860cf1acefc38653732c850d7c4f1b Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Tue, 10 Sep 2013 18:22:31 +0100 Subject: [PATCH 49/80] Add photo list filters --- tests/functional/test_photos.py | 16 ++++++++++++++++ tests/unit/test_photos.py | 13 +++++++++++++ trovebox/api/api_photo.py | 11 +++++++---- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/tests/functional/test_photos.py b/tests/functional/test_photos.py index 19f4bb5..2540953 100644 --- a/tests/functional/test_photos.py +++ b/tests/functional/test_photos.py @@ -6,6 +6,22 @@ from tests.functional import test_base class TestPhotos(test_base.TestBase): testcase_name = "photo API" + def test_list_filter(self): + """ + Check that the photo list filter parameter works correctly + """ + filter_tag = "Filter" + # Assign a photo with a new tag + self.photos[0].update(tagsAdd=filter_tag) + + # Check that the photos can be filtered + photos = self.client.photos.list(filters={"tags": filter_tag}) + self.assertEqual(len(photos), 1) + self.assertEqual(photos[0].id, self.photos[0].id) + + # Put the environment back the way we found it + photos[0].update(tagsRemove=filter_tag) + def test_delete_upload(self): """ Test photo deletion and upload """ # Delete one photo using the Trovebox class, passing in the id diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index d549f12..f55ba61 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -55,6 +55,19 @@ class TestPhotosList(TestPhotos): mock_get.assert_called_with("/photos/list.json", foo="bar") self.assertEqual(result, []) + @mock.patch.object(trovebox.Trovebox, 'get') + def test_filters(self, mock_get): + """Check that the activity list filters are applied properly""" + mock_get.return_value = self._return_value(self.test_photos_dict) + self.client.photos.list(filters={"foo": "bar", + "test1": "test2"}, + foo="bar") + # Dict element can be any order + self.assertIn(mock_get.call_args[0], + [("/photos/foo-bar/test1-test2/list.json",), + ("/photos/test1-test2/foo-bar/list.json",)]) + self.assertEqual(mock_get.call_args[1], {"foo": "bar"}) + class TestPhotosUpdate(TestPhotos): @mock.patch.object(trovebox.Trovebox, 'post') def test_photos_update(self, mock_post): diff --git a/trovebox/api/api_photo.py b/trovebox/api/api_photo.py index edf5338..8633969 100644 --- a/trovebox/api/api_photo.py +++ b/trovebox/api/api_photo.py @@ -9,14 +9,17 @@ from .api_base import ApiBase class ApiPhotos(ApiBase): """ Definitions of /photos/ API endpoints """ - # TODO: Add options - def list(self, **kwds): + def list(self, filters=None, **kwds): """ - Endpoint: /photos/list.json + Endpoint: /photos/[]/list.json Returns a list of Photo objects. + The filters parameter can be used to narrow down the list. + Eg: filters={"album": } """ - photos = self._client.get("/photos/list.json", **kwds)["result"] + filter_string = self._build_filter_string(filters) + photos = self._client.get("/photos/%slist.json" % filter_string, + **kwds)["result"] photos = self._result_to_list(photos) return [Photo(self._client, photo) for photo in photos] From 6b3f01092075da0040e28c07cf24143b1f0f7f9a Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Wed, 11 Sep 2013 17:27:21 +0100 Subject: [PATCH 50/80] Added photos/share endpoint (currently not implemented in frontend) --- tests/functional/test_photos.py | 11 +++++++++++ tests/unit/test_photos.py | 12 ++++++++++++ trovebox/api/api_photo.py | 10 +++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_photos.py b/tests/functional/test_photos.py index 2540953..92b6cd5 100644 --- a/tests/functional/test_photos.py +++ b/tests/functional/test_photos.py @@ -1,5 +1,10 @@ from __future__ import unicode_literals +try: + import unittest2 as unittest # Python2.6 +except ImportError: + import unittest + import trovebox from tests.functional import test_base @@ -22,6 +27,12 @@ class TestPhotos(test_base.TestBase): # Put the environment back the way we found it photos[0].update(tagsRemove=filter_tag) + # Photo share endpoint is currently not implemented + @unittest.expectedFailure + def test_share(self): + """ Test photo sharing (currently not implemented) """ + self.client.photos.share() + def test_delete_upload(self): """ Test photo deletion and upload """ # Delete one photo using the Trovebox class, passing in the id diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index f55ba61..d9c7899 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -68,6 +68,18 @@ class TestPhotosList(TestPhotos): ("/photos/test1-test2/foo-bar/list.json",)]) self.assertEqual(mock_get.call_args[1], {"foo": "bar"}) +class TestPhotosList(TestPhotos): + @mock.patch.object(trovebox.Trovebox, 'post') + def test_photos_share(self, mock_post): + self.client.photos.share(filters={"foo": "bar", + "test1": "test2"}, + foo="bar") + # Dict element can be any order + self.assertIn(mock_post.call_args[0], + [("/photos/foo-bar/test1-test2/share.json",), + ("/photos/test1-test2/foo-bar/share.json",)]) + self.assertEqual(mock_post.call_args[1], {"foo": "bar"}) + class TestPhotosUpdate(TestPhotos): @mock.patch.object(trovebox.Trovebox, 'post') def test_photos_update(self, mock_post): diff --git a/trovebox/api/api_photo.py b/trovebox/api/api_photo.py index 8633969..006da7c 100644 --- a/trovebox/api/api_photo.py +++ b/trovebox/api/api_photo.py @@ -23,7 +23,15 @@ class ApiPhotos(ApiBase): photos = self._result_to_list(photos) return [Photo(self._client, photo) for photo in photos] - # def share(self, **kwds): + def share(self, filters=None, **kwds): + """ + Endpoint: /photos/[/share.json + + Not currently implemented. + """ + filter_string = self._build_filter_string(filters) + return self._client.post("/photos/%sshare.json" % filter_string, + **kwds)["result"] def delete(self, photos, **kwds): """ From bf437ffc7e37cd30e720388735ecf4d57f548276 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Wed, 11 Sep 2013 17:47:09 +0100 Subject: [PATCH 51/80] Don't explicitly raise a TroveboxError if False is returned, rely on frontend to issue an error code. This also means we don't need to test for this in unit tests. --- tests/unit/test_actions.py | 17 ---------------- tests/unit/test_activities.py | 7 ------- tests/unit/test_albums.py | 17 ---------------- tests/unit/test_photos.py | 37 ----------------------------------- tests/unit/test_tags.py | 17 ---------------- trovebox/api/api_action.py | 9 +++------ trovebox/api/api_activity.py | 8 ++++---- trovebox/api/api_album.py | 9 +++------ trovebox/api/api_photo.py | 23 +++++++--------------- trovebox/api/api_tag.py | 9 +++------ 10 files changed, 20 insertions(+), 133 deletions(-) diff --git a/tests/unit/test_actions.py b/tests/unit/test_actions.py index 8da4bb6..ffb83dc 100644 --- a/tests/unit/test_actions.py +++ b/tests/unit/test_actions.py @@ -98,13 +98,6 @@ class TestActionDelete(TestActions): mock_post.assert_called_with("/action/1/delete.json", foo="bar") self.assertEqual(result, True) - @mock.patch.object(trovebox.Trovebox, 'post') - def test_action_delete_failure(self, mock_post): - """Check that an exception is raised if an action cannot be deleted""" - mock_post.return_value = self._return_value(False) - with self.assertRaises(trovebox.TroveboxError): - self.client.action.delete(self.test_actions[0]) - @mock.patch.object(trovebox.Trovebox, 'post') def test_action_object_delete(self, mock_post): """Check that an action can be deleted using the action object directly""" @@ -116,16 +109,6 @@ class TestActionDelete(TestActions): self.assertEqual(action.get_fields(), {}) self.assertEqual(action.id, None) - @mock.patch.object(trovebox.Trovebox, 'post') - def test_action_object_delete_failure(self, mock_post): - """ - Check that an exception is raised if an action cannot be deleted - when using the action object directly - """ - mock_post.return_value = self._return_value(False) - with self.assertRaises(trovebox.TroveboxError): - self.test_actions[0].delete() - class TestActionView(TestActions): @mock.patch.object(trovebox.Trovebox, 'get') def test_action_view(self, mock_get): diff --git a/tests/unit/test_activities.py b/tests/unit/test_activities.py index d756ff6..d77f460 100644 --- a/tests/unit/test_activities.py +++ b/tests/unit/test_activities.py @@ -91,13 +91,6 @@ class TestActivitiesPurge(TestActivities): mock_get.assert_called_with("/activities/purge.json", foo="bar") self.assertEqual(result, True) - @mock.patch.object(trovebox.Trovebox, 'post') - def test_activity_purge_failure(self, mock_post): - """Test activity purging """ - mock_post.return_value = self._return_value(False) - with self.assertRaises(trovebox.TroveboxError): - result = self.client.activities.purge(foo="bar") - class TestActivityView(TestActivities): @mock.patch.object(trovebox.Trovebox, 'get') def test_activity_view(self, mock_get): diff --git a/tests/unit/test_albums.py b/tests/unit/test_albums.py index 3469a25..811c39f 100644 --- a/tests/unit/test_albums.py +++ b/tests/unit/test_albums.py @@ -147,13 +147,6 @@ class TestAlbumDelete(TestAlbums): mock_post.assert_called_with("/album/1/delete.json", foo="bar") self.assertEqual(result, True) - @mock.patch.object(trovebox.Trovebox, 'post') - def test_album_delete_failure(self, mock_post): - """Check that an exception is raised if an album cannot be deleted""" - mock_post.return_value = self._return_value(False) - with self.assertRaises(trovebox.TroveboxError): - self.client.album.delete(self.test_albums[0]) - @mock.patch.object(trovebox.Trovebox, 'post') def test_album_object_delete(self, mock_post): """Check that an album can be deleted using the album object directly""" @@ -166,16 +159,6 @@ class TestAlbumDelete(TestAlbums): self.assertEqual(album.id, None) self.assertEqual(album.name, None) - @mock.patch.object(trovebox.Trovebox, 'post') - def test_album_object_delete_failure(self, mock_post): - """ - Check that an exception is raised if an album cannot be deleted - when using the album object directly - """ - mock_post.return_value = self._return_value(False) - with self.assertRaises(trovebox.TroveboxError): - self.test_albums[0].delete() - class TestAlbumAdd(TestAlbums): @mock.patch.object(trovebox.Trovebox, 'post') def test_album_add(self, mock_post): diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index d9c7899..4107c8e 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -99,16 +99,6 @@ class TestPhotosUpdate(TestPhotos): ids=["1a", "2b"], title="Test") self.assertEqual(result, True) - @mock.patch.object(trovebox.Trovebox, 'post') - def test_photos_update_failure(self, mock_post): - """ - Check that an exception is raised if multiple photos - cannot be updated - """ - mock_post.return_value = self._return_value(False) - with self.assertRaises(trovebox.TroveboxError): - self.client.photos.update(self.test_photos, title="Test") - class TestPhotosDelete(TestPhotos): @mock.patch.object(trovebox.Trovebox, 'post') def test_photos_delete(self, mock_post): @@ -128,16 +118,6 @@ class TestPhotosDelete(TestPhotos): ids=["1a", "2b"], foo="bar") self.assertEqual(result, True) - @mock.patch.object(trovebox.Trovebox, 'post') - def test_photos_delete_failure(self, mock_post): - """ - Check that an exception is raised if multiple photos - cannot be deleted - """ - mock_post.return_value = self._return_value(False) - with self.assertRaises(trovebox.TroveboxError): - self.client.photos.delete(self.test_photos) - class TestPhotoDelete(TestPhotos): @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_delete(self, mock_post): @@ -155,13 +135,6 @@ class TestPhotoDelete(TestPhotos): mock_post.assert_called_with("/photo/1a/delete.json", foo="bar") self.assertEqual(result, True) - @mock.patch.object(trovebox.Trovebox, 'post') - def test_photo_delete_failure(self, mock_post): - """Check that an exception is raised if a photo cannot be deleted""" - mock_post.return_value = self._return_value(False) - with self.assertRaises(trovebox.TroveboxError): - self.client.photo.delete(self.test_photos[0]) - @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_object_delete(self, mock_post): """ @@ -176,16 +149,6 @@ class TestPhotoDelete(TestPhotos): self.assertEqual(photo.get_fields(), {}) self.assertEqual(photo.id, None) - @mock.patch.object(trovebox.Trovebox, 'post') - def test_photo_object_delete_failure(self, mock_post): - """ - Check that an exception is raised if a photo cannot be deleted - when using the photo object directly - """ - mock_post.return_value = self._return_value(False) - with self.assertRaises(trovebox.TroveboxError): - self.test_photos[0].delete() - class TestPhotoReplace(TestPhotos): @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_replace(self, _): diff --git a/tests/unit/test_tags.py b/tests/unit/test_tags.py index 47a71e4..61292d4 100644 --- a/tests/unit/test_tags.py +++ b/tests/unit/test_tags.py @@ -78,13 +78,6 @@ class TestTagDelete(TestTags): mock_post.assert_called_with("/tag/tag1/delete.json", foo="bar") self.assertEqual(result, True) - @mock.patch.object(trovebox.Trovebox, 'post') - def test_tag_delete_failure(self, mock_post): - """Check that an exception is raised if a tag cannot be deleted""" - mock_post.return_value = self._return_value(False) - with self.assertRaises(trovebox.TroveboxError): - self.client.tag.delete(self.test_tags[0]) - @mock.patch.object(trovebox.Trovebox, 'post') def test_tag_object_delete(self, mock_post): """Check that a tag can be deleted when using the tag object directly""" @@ -96,16 +89,6 @@ class TestTagDelete(TestTags): self.assertEqual(tag.get_fields(), {}) self.assertEqual(tag.id, None) - @mock.patch.object(trovebox.Trovebox, 'post') - def test_tag_object_delete_failure(self, mock_post): - """ - Check that an exception is raised if a tag cannot be deleted - when using the tag object directly - """ - mock_post.return_value = self._return_value(False) - with self.assertRaises(trovebox.TroveboxError): - self.test_tags[0].delete() - class TestTagUpdate(TestTags): @mock.patch.object(trovebox.Trovebox, 'post') def test_tag_update(self, mock_post): diff --git a/trovebox/api/api_action.py b/trovebox/api/api_action.py index a1dda45..10a7d09 100644 --- a/trovebox/api/api_action.py +++ b/trovebox/api/api_action.py @@ -39,12 +39,9 @@ class ApiAction(ApiBase): Returns True if successful. Raises a TroveboxError if not. """ - result = self._client.post("/action/%s/delete.json" % - self._extract_id(action), - **kwds)["result"] - if not result: - raise TroveboxError("Delete response returned False") - return result + return self._client.post("/action/%s/delete.json" % + self._extract_id(action), + **kwds)["result"] def view(self, action, **kwds): """ diff --git a/trovebox/api/api_activity.py b/trovebox/api/api_activity.py index bf2e63e..88aef5c 100644 --- a/trovebox/api/api_activity.py +++ b/trovebox/api/api_activity.py @@ -27,11 +27,11 @@ class ApiActivities(ApiBase): Endpoint: /activities/purge.json Purges all activities. - Currently not working due to frontend issue #1368 + Returns True if successful. + Raises a TroveboxError if not. + Currently not working due to frontend issue #1368. """ - if not self._client.post("/activities/purge.json", **kwds)["result"]: - raise TroveboxError("Purge response returned False") - return True + return self._client.post("/activities/purge.json", **kwds)["result"] class ApiActivity(ApiBase): """ Definitions of /activity/ API endpoints """ diff --git a/trovebox/api/api_album.py b/trovebox/api/api_album.py index 87679f4..28fa23e 100644 --- a/trovebox/api/api_album.py +++ b/trovebox/api/api_album.py @@ -60,12 +60,9 @@ class ApiAlbum(ApiBase): Returns True if successful. Raises a TroveboxError if not. """ - result = self._client.post("/album/%s/delete.json" % - self._extract_id(album), - **kwds)["result"] - if not result: - raise TroveboxError("Delete response returned False") - return result + return self._client.post("/album/%s/delete.json" % + self._extract_id(album), + **kwds)["result"] def add(self, album, objects, object_type=None, **kwds): """ diff --git a/trovebox/api/api_photo.py b/trovebox/api/api_photo.py index 006da7c..334d37a 100644 --- a/trovebox/api/api_photo.py +++ b/trovebox/api/api_photo.py @@ -42,10 +42,8 @@ class ApiPhotos(ApiBase): Raises a TroveboxError if not. """ ids = [self._extract_id(photo) for photo in photos] - if not self._client.post("/photos/delete.json", ids=ids, - **kwds)["result"]: - raise TroveboxError("Delete response returned False") - return True + return self._client.post("/photos/delete.json", ids=ids, + **kwds)["result"] def update(self, photos, **kwds): """ @@ -56,10 +54,8 @@ class ApiPhotos(ApiBase): Raises TroveboxError if not. """ ids = [self._extract_id(photo) for photo in photos] - if not self._client.post("/photos/update.json", ids=ids, - **kwds)["result"]: - raise TroveboxError("Update response returned False") - return True + return self._client.post("/photos/update.json", ids=ids, + **kwds)["result"] class ApiPhoto(ApiBase): """ Definitions of /photo/ API endpoints """ @@ -71,14 +67,9 @@ class ApiPhoto(ApiBase): Returns True if successful. Raises a TroveboxError if not. """ - result = self._client.post("/photo/%s/delete.json" % - self._extract_id(photo), - **kwds)["result"] - if not result: - raise TroveboxError("Delete response returned False") - return result - - # def delete_source(self, photo, **kwds): + return self._client.post("/photo/%s/delete.json" % + self._extract_id(photo), + **kwds)["result"] def replace(self, photo, photo_file, **kwds): """ Not yet implemented """ diff --git a/trovebox/api/api_tag.py b/trovebox/api/api_tag.py index 6f8e513..1274125 100644 --- a/trovebox/api/api_tag.py +++ b/trovebox/api/api_tag.py @@ -42,12 +42,9 @@ class ApiTag(ApiBase): Returns True if successful. Raises a TroveboxError if not. """ - result = self._client.post("/tag/%s/delete.json" % - quote(self._extract_id(tag)), - **kwds)["result"] - if not result: - raise TroveboxError("Delete response returned False") - return result + return self._client.post("/tag/%s/delete.json" % + quote(self._extract_id(tag)), + **kwds)["result"] def update(self, tag, **kwds): """ From dc03fd2dfc6f80671085fd3a40c78bf65496f9a4 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Wed, 11 Sep 2013 18:08:06 +0100 Subject: [PATCH 52/80] Truncate response logging to 1000 characters --- trovebox/http.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/trovebox/http.py b/trovebox/http.py index 21e4c69..7f6f666 100644 --- a/trovebox/http.py +++ b/trovebox/http.py @@ -104,7 +104,9 @@ class Http(object): self._logger.info("============================") self._logger.info("GET %s" % url) self._logger.info("---") - self._logger.info(response.text) + self._logger.info(response.text[:1000]) + if len(response.text) > 1000: + self._logger.info("[Response truncated to 1000 characters]") self.last_url = url self.last_params = params @@ -158,7 +160,9 @@ class Http(object): if files: self._logger.info("files: %s" % repr(files)) self._logger.info("---") - self._logger.info(response.text) + self._logger.info(response.text[:1000]) + if len(response.text) > 1000: + self._logger.info("[Response truncated to 1000 characters]") self.last_url = url self.last_params = params From 3da64a59cc2dcb1db766d1ce63c5df6e3686c0c0 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Wed, 11 Sep 2013 18:15:55 +0100 Subject: [PATCH 53/80] Add photo.delete_source endpoint --- tests/functional/test_photos.py | 16 ++++++++++++++++ tests/unit/test_photos.py | 29 +++++++++++++++++++++++++++++ trovebox/api/api_photo.py | 12 ++++++++++++ trovebox/objects/photo.py | 10 +++++++++- 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_photos.py b/tests/functional/test_photos.py index 92b6cd5..2ac225a 100644 --- a/tests/functional/test_photos.py +++ b/tests/functional/test_photos.py @@ -74,6 +74,22 @@ class TestPhotos(test_base.TestBase): self._delete_all() self._create_test_photos() + def test_delete_source(self): + """ Test that photo source files can be deleted """ + # Upload a new (duplicate) photo + photo = self.client.photo.upload("tests/data/test_photo1.jpg", + allowDuplicate=True) + # Check that the photo can be downloaded + self.client.get("photo/%s/download" % photo.id, process_response=False) + + # Delete the source and check that the source file no longer exists + photo.delete_source() + with self.assertRaises(trovebox.TroveboxError): + self.client.get("photo/%s/download" % photo.id, process_response=False) + + # Put the environment back the way we found it + photo.delete() + def test_upload_duplicate(self): """ Ensure that duplicate photos are rejected """ # Attempt to upload a duplicate diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index 4107c8e..ecc1604 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -149,6 +149,35 @@ class TestPhotoDelete(TestPhotos): self.assertEqual(photo.get_fields(), {}) self.assertEqual(photo.id, None) +class TestPhotoDeleteSource(TestPhotos): + @mock.patch.object(trovebox.Trovebox, 'post') + def test_photo_delete_source(self, mock_post): + """Check that photo source files can be deleted""" + mock_post.return_value = self._return_value(True) + result = self.client.photo.delete_source(self.test_photos[0], foo="bar") + mock_post.assert_called_with("/photo/1a/source/delete.json", foo="bar") + self.assertEqual(result, True) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_photo_delete_source_id(self, mock_post): + """Check that photo source files can be deleted using its ID""" + mock_post.return_value = self._return_value(True) + result = self.client.photo.delete_source("1a", foo="bar") + mock_post.assert_called_with("/photo/1a/source/delete.json", foo="bar") + self.assertEqual(result, True) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_photo_object_delete_source(self, mock_post): + """ + Check that photo source files can be deleted when using + the photo object directly + """ + mock_post.return_value = self._return_value(True) + photo = self.test_photos[0] + result = photo.delete_source(foo="bar") + mock_post.assert_called_with("/photo/1a/source/delete.json", foo="bar") + self.assertEqual(result, True) + class TestPhotoReplace(TestPhotos): @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_replace(self, _): diff --git a/trovebox/api/api_photo.py b/trovebox/api/api_photo.py index 334d37a..7a3d42a 100644 --- a/trovebox/api/api_photo.py +++ b/trovebox/api/api_photo.py @@ -71,6 +71,18 @@ class ApiPhoto(ApiBase): self._extract_id(photo), **kwds)["result"] + def delete_source(self, photo, **kwds): + """ + Endpoint: /photo//source/delete.json + + Delete the source files of a photo. + Returns True if successful. + Raises a TroveboxError if not. + """ + return self._client.post("/photo/%s/source/delete.json" % + self._extract_id(photo), + **kwds)["result"] + def replace(self, photo, photo_file, **kwds): """ Not yet implemented """ raise NotImplementedError() diff --git a/trovebox/objects/photo.py b/trovebox/objects/photo.py index 28865c5..2f4987c 100644 --- a/trovebox/objects/photo.py +++ b/trovebox/objects/photo.py @@ -19,7 +19,15 @@ class Photo(TroveboxObject): self._delete_fields() return result - # def delete_source(self, **kwds): + def delete_source(self, **kwds): + """ + Endpoint: /photo//source/delete.json + + Deletes the source files of this photo. + Returns True if successful. + Raises a TroveboxError if not. + """ + return self._client.photo.delete_source(self, **kwds) def replace(self, photo_file, **kwds): """ Not implemented yet """ From c635c7c0c0800d780dda879f3d7f88814e95fd30 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Thu, 12 Sep 2013 17:53:16 +0100 Subject: [PATCH 54/80] Add photo replace endpoints --- tests/functional/test_photos.py | 15 ++++- tests/unit/test_photos.py | 116 +++++++++++++++++++++++++------- trovebox/api/api_photo.py | 32 +++++++-- trovebox/objects/photo.py | 20 ++++-- 4 files changed, 147 insertions(+), 36 deletions(-) diff --git a/tests/functional/test_photos.py b/tests/functional/test_photos.py index 2ac225a..34b91ab 100644 --- a/tests/functional/test_photos.py +++ b/tests/functional/test_photos.py @@ -170,9 +170,18 @@ class TestPhotos(test_base.TestBase): self.assertEqual(next_prev["next"][0].id, self.photos[2].id) def test_replace(self): - """ If photo.replace gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.client.photo.replace(None, None) + """ Test that a photo can be replaced with another """ + # Replace the first photo with a copy of the second + original_hash = self.photos[0].hash + self.assertNotEqual(original_hash, self.photos[1].hash) + self.photos[0].replace("tests/data/test_photo2.jpg", + allowDuplicate=True) + # Check that its new hash is correct + self.assertEqual(self.photos[0].hash, self.photos[1].hash) + # Put it back + self.photos[0].replace("tests/data/test_photo1.jpg", + allowDuplicate=True) + self.assertEqual(self.photos[0].hash, original_hash) def test_replace_encoded(self): """ If photo.replace_encoded gets implemented, write a test! """ diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index ecc1604..4415ccd 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -180,41 +180,107 @@ class TestPhotoDeleteSource(TestPhotos): class TestPhotoReplace(TestPhotos): @mock.patch.object(trovebox.Trovebox, 'post') - def test_photo_replace(self, _): - """ If photo.replace gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.client.photo.replace(self.test_photos[0], self.test_file) + def test_photo_replace(self, mock_post): + """Check that an existing photo can be replaced""" + mock_post.return_value = self._return_value(self.test_photos_dict[0]) + result = self.client.photo.replace(self.test_photos[1], + self.test_file, title="Test") + # It's not possible to compare the file object, + # so check each parameter individually + endpoint = mock_post.call_args[0] + title = mock_post.call_args[1]["title"] + files = mock_post.call_args[1]["files"] + self.assertEqual(endpoint, + ("/photo/%s/replace.json" % self.test_photos[1].id,)) + self.assertEqual(title, "Test") + self.assertIn("photo", files) + self.assertEqual(result.get_fields(), self.test_photos_dict[0]) @mock.patch.object(trovebox.Trovebox, 'post') - def test_photo_replace_id(self, _): - """ If photo.replace gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.client.photo.replace("1a", self.test_file) + def test_photo_replace_id(self, mock_post): + """Check that an existing photo can be replaced using its ID""" + mock_post.return_value = self._return_value(self.test_photos_dict[0]) + result = self.client.photo.replace(self.test_photos[1].id, + self.test_file, title="Test") + # It's not possible to compare the file object, + # so check each parameter individually + endpoint = mock_post.call_args[0] + title = mock_post.call_args[1]["title"] + files = mock_post.call_args[1]["files"] + self.assertEqual(endpoint, + ("/photo/%s/replace.json" % self.test_photos[1].id,)) + self.assertEqual(title, "Test") + self.assertIn("photo", files) + self.assertEqual(result.get_fields(), self.test_photos_dict[0]) @mock.patch.object(trovebox.Trovebox, 'post') - def test_photo_object_replace(self, _): - """ If photo.replace gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.test_photos[0].replace(self.test_file) + def test_photo_object_replace(self, mock_post): + """ + Check that an existing photo can be replaced when using the + Photo object directly. + """ + photo_id = self.test_photos[1].id + mock_post.return_value = self._return_value(self.test_photos_dict[0]) + self.test_photos[1].replace(self.test_file, title="Test") + # It's not possible to compare the file object, + # so check each parameter individually + endpoint = mock_post.call_args[0] + title = mock_post.call_args[1]["title"] + files = mock_post.call_args[1]["files"] + self.assertEqual(endpoint, ("/photo/%s/replace.json" % photo_id,)) + self.assertEqual(title, "Test") + self.assertIn("photo", files) + self.assertEqual(self.test_photos[1].get_fields(), + self.test_photos_dict[0]) @mock.patch.object(trovebox.Trovebox, 'post') - def test_photo_replace_encoded(self, _): - """ If photo.replace_encoded gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.client.photo.replace_encoded(self.test_photos[0], - self.test_file) + def test_photo_replace_encoded(self, mock_post): + """ + Check that a photo can be uploaded using Base64 encoding to + replace an existing photo. + """ + mock_post.return_value = self._return_value(self.test_photos_dict[0]) + result = self.client.photo.replace_encoded(self.test_photos[1], + self.test_file, title="Test") + with open(self.test_file, "rb") as in_file: + encoded_file = base64.b64encode(in_file.read()) + mock_post.assert_called_with("/photo/%s/replace.json" + % self.test_photos[1].id, + photo=encoded_file, title="Test") + self.assertEqual(result.get_fields(), self.test_photos_dict[0]) @mock.patch.object(trovebox.Trovebox, 'post') - def test_photo_replace_encoded_id(self, _): - """ If photo.replace_encoded gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.client.photo.replace_encoded("1a", self.test_file) + def test_photo_replace_encoded_id(self, mock_post): + """ + Check that a photo can be uploaded using Base64 encoding to + replace an existing photo using its ID. + """ + mock_post.return_value = self._return_value(self.test_photos_dict[0]) + result = self.client.photo.replace_encoded(self.test_photos[1].id, + self.test_file, title="Test") + with open(self.test_file, "rb") as in_file: + encoded_file = base64.b64encode(in_file.read()) + mock_post.assert_called_with("/photo/%s/replace.json" + % self.test_photos[1].id, + photo=encoded_file, title="Test") + self.assertEqual(result.get_fields(), self.test_photos_dict[0]) @mock.patch.object(trovebox.Trovebox, 'post') - def test_photo_object_replace_encoded(self, _): - """ If photo.replace_encoded gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.test_photos[0].replace_encoded(photo_file=self.test_file) + def test_photo_object_replace_encoded(self, mock_post): + """ + Check that a photo can be uploaded using Base64 encoding to + replace an existing photo when using the Photo object directly. + """ + photo_id = self.test_photos[1].id + mock_post.return_value = self._return_value(self.test_photos_dict[0]) + self.test_photos[1].replace_encoded(self.test_file, title="Test") + with open(self.test_file, "rb") as in_file: + encoded_file = base64.b64encode(in_file.read()) + mock_post.assert_called_with("/photo/%s/replace.json" + % photo_id, + photo=encoded_file, title="Test") + self.assertEqual(self.test_photos[1].get_fields(), + self.test_photos_dict[0]) class TestPhotoUpdate(TestPhotos): @mock.patch.object(trovebox.Trovebox, 'post') diff --git a/trovebox/api/api_photo.py b/trovebox/api/api_photo.py index 7a3d42a..8f3286e 100644 --- a/trovebox/api/api_photo.py +++ b/trovebox/api/api_photo.py @@ -84,12 +84,34 @@ class ApiPhoto(ApiBase): **kwds)["result"] def replace(self, photo, photo_file, **kwds): - """ Not yet implemented """ - raise NotImplementedError() + """ + Endpoint: /photo//replace.json + + Uploads the specified photo file to replace an existing photo. + """ + with open(photo_file, 'rb') as in_file: + result = self._client.post("/photo/%s/replace.json" % + self._extract_id(photo), + files={'photo': in_file}, + **kwds)["result"] + return Photo(self._client, result) def replace_encoded(self, photo, photo_file, **kwds): - """ Not yet implemented """ - raise NotImplementedError() + """ + Endpoint: /photo//replace.json + + Base64-encodes and uploads the specified photo filename to + replace an existing photo. + """ + with open(photo_file, "rb") as in_file: + encoded_photo = base64.b64encode(in_file.read()) + result = self._client.post("/photo/%s/replace.json" % + self._extract_id(photo), + photo=encoded_photo, + **kwds)["result"] + return Photo(self._client, result) + +# def replace_from_url(self, url, **kwds): def update(self, photo, **kwds): """ @@ -142,6 +164,8 @@ class ApiPhoto(ApiBase): **kwds)["result"] return Photo(self._client, result) +# def upload_from_url(self, url, **kwds): + def dynamic_url(self, photo, **kwds): """ Not yet implemented """ raise NotImplementedError() diff --git a/trovebox/objects/photo.py b/trovebox/objects/photo.py index 2f4987c..89aa769 100644 --- a/trovebox/objects/photo.py +++ b/trovebox/objects/photo.py @@ -30,12 +30,24 @@ class Photo(TroveboxObject): return self._client.photo.delete_source(self, **kwds) def replace(self, photo_file, **kwds): - """ Not implemented yet """ - raise NotImplementedError() + """ + Endpoint: /photo//replace.json + + Uploads the specified photo file to replace this photo. + """ + result = self._client.photo.replace(self, photo_file, **kwds) + self._replace_fields(result.get_fields()) def replace_encoded(self, photo_file, **kwds): - """ Not implemented yet """ - raise NotImplementedError() + """ + Endpoint: /photo//replace.json + + Base64-encodes and uploads the specified photo file to + replace this photo. + """ + result = self._client.photo.replace_encoded(self, photo_file, + **kwds) + self._replace_fields(result.get_fields()) def update(self, **kwds): """ From f1a9018918c199221e6259cc7afb471f96d65879 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Thu, 12 Sep 2013 17:54:42 +0100 Subject: [PATCH 55/80] Remove unused imports --- trovebox/api/api_action.py | 1 - trovebox/api/api_activity.py | 1 - trovebox/api/api_album.py | 1 - trovebox/api/api_photo.py | 1 - trovebox/api/api_tag.py | 1 - 5 files changed, 5 deletions(-) diff --git a/trovebox/api/api_action.py b/trovebox/api/api_action.py index 10a7d09..bf6a7e3 100644 --- a/trovebox/api/api_action.py +++ b/trovebox/api/api_action.py @@ -1,7 +1,6 @@ """ api_action.py : Trovebox Action API Classes """ -from trovebox.errors import TroveboxError from trovebox.objects.action import Action from .api_base import ApiBase diff --git a/trovebox/api/api_activity.py b/trovebox/api/api_activity.py index 88aef5c..cd8e308 100644 --- a/trovebox/api/api_activity.py +++ b/trovebox/api/api_activity.py @@ -2,7 +2,6 @@ api_activity.py : Trovebox Activity API Classes """ import json -from trovebox.errors import TroveboxError from trovebox.objects.activity import Activity from .api_base import ApiBase diff --git a/trovebox/api/api_album.py b/trovebox/api/api_album.py index 28fa23e..216b60b 100644 --- a/trovebox/api/api_album.py +++ b/trovebox/api/api_album.py @@ -3,7 +3,6 @@ api_album.py : Trovebox Album API Classes """ import collections -from trovebox.errors import TroveboxError from trovebox.objects.trovebox_object import TroveboxObject from trovebox.objects.album import Album from .api_base import ApiBase diff --git a/trovebox/api/api_photo.py b/trovebox/api/api_photo.py index 8f3286e..bffb0cb 100644 --- a/trovebox/api/api_photo.py +++ b/trovebox/api/api_photo.py @@ -3,7 +3,6 @@ api_photo.py : Trovebox Photo API Classes """ import base64 -from trovebox.errors import TroveboxError from trovebox.objects.photo import Photo from .api_base import ApiBase diff --git a/trovebox/api/api_tag.py b/trovebox/api/api_tag.py index 1274125..898cffe 100644 --- a/trovebox/api/api_tag.py +++ b/trovebox/api/api_tag.py @@ -6,7 +6,6 @@ try: except ImportError: from urllib import quote # Python2 -from trovebox.errors import TroveboxError from trovebox.objects.tag import Tag from .api_base import ApiBase From 35b9a4d0448b4d9f2b474d68f5381e2cf3680784 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Thu, 12 Sep 2013 18:26:51 +0100 Subject: [PATCH 56/80] Add upload/replace from URL. --- tests/functional/test_photos.py | 28 +++++++++++----- tests/unit/test_photos.py | 58 +++++++++++++++++++++++++++++++++ trovebox/api/api_photo.py | 24 ++++++++++++-- trovebox/objects/photo.py | 9 +++++ 4 files changed, 109 insertions(+), 10 deletions(-) diff --git a/tests/functional/test_photos.py b/tests/functional/test_photos.py index 34b91ab..7b8899d 100644 --- a/tests/functional/test_photos.py +++ b/tests/functional/test_photos.py @@ -101,6 +101,23 @@ class TestPhotos(test_base.TestBase): self.photos = self.client.photos.list() self.assertEqual(len(self.photos), 3) + def test_upload_from_url(self): + """ Ensure that a photo can be imported from a URL """ + # Make an existing photo public + self.photos[0].update(permission=True) + # Upload a duplicate of an existing photo + self.client.photo.upload_from_url(self.photos[0].pathDownload, + allowDuplicate=True) + # Check there are now four photos + photos = self.client.photos.list() + self.assertEqual(len(photos), 4) + # Check that the new one is a duplicate + self.assertEqual(photos[0].hash, photos[1].hash) + + # Put the environment back the way we found it + photos[1].delete() + self.photos[0].update(permission=False) + def test_update(self): """ Update a photo by editing the title """ title = "\xfcmlaut" # umlauted umlaut @@ -178,16 +195,11 @@ class TestPhotos(test_base.TestBase): allowDuplicate=True) # Check that its new hash is correct self.assertEqual(self.photos[0].hash, self.photos[1].hash) - # Put it back - self.photos[0].replace("tests/data/test_photo1.jpg", - allowDuplicate=True) + # Put it back using base64 encoding + self.photos[0].replace_encoded("tests/data/test_photo1.jpg", + allowDuplicate=True) self.assertEqual(self.photos[0].hash, original_hash) - def test_replace_encoded(self): - """ If photo.replace_encoded gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.client.photo.replace_encoded(None, None) - def test_dynamic_url(self): """ If photo.dynamic_url gets implemented, write a test! """ with self.assertRaises(NotImplementedError): diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index 4415ccd..349843a 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -233,6 +233,7 @@ class TestPhotoReplace(TestPhotos): self.assertEqual(self.test_photos[1].get_fields(), self.test_photos_dict[0]) +class TestPhotoReplaceEncoded(TestPhotos): @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_replace_encoded(self, mock_post): """ @@ -282,6 +283,50 @@ class TestPhotoReplace(TestPhotos): self.assertEqual(self.test_photos[1].get_fields(), self.test_photos_dict[0]) +class TestPhotoReplaceFromUrl(TestPhotos): + @mock.patch.object(trovebox.Trovebox, 'post') + def test_photo_replace_from_url(self, mock_post): + """ + Check that a photo can be imported from a url to + replace an existing photo. + """ + mock_post.return_value = self._return_value(self.test_photos_dict[0]) + result = self.client.photo.replace_from_url(self.test_photos[1], + "test_url", title="Test") + mock_post.assert_called_with("/photo/%s/replace.json" + % self.test_photos[1].id, + photo="test_url", title="Test") + self.assertEqual(result.get_fields(), self.test_photos_dict[0]) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_photo_id_replace_from_url(self, mock_post): + """ + Check that a photo can be imported from a url to + replace an existing photo using its ID. + """ + mock_post.return_value = self._return_value(self.test_photos_dict[0]) + result = self.client.photo.replace_from_url(self.test_photos[1].id, + "test_url", title="Test") + mock_post.assert_called_with("/photo/%s/replace.json" + % self.test_photos[1].id, + photo="test_url", title="Test") + self.assertEqual(result.get_fields(), self.test_photos_dict[0]) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_photo_object_replace_from_url(self, mock_post): + """ + Check that a photo can be imported from a url to + replace an existing photo when using the Photo object directly. + """ + photo_id = self.test_photos[1].id + mock_post.return_value = self._return_value(self.test_photos_dict[0]) + self.test_photos[1].replace_from_url("test_url", title="Test") + mock_post.assert_called_with("/photo/%s/replace.json" + % photo_id, + photo="test_url", title="Test") + self.assertEqual(self.test_photos[1].get_fields(), + self.test_photos_dict[0]) + class TestPhotoUpdate(TestPhotos): @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_update(self, mock_post): @@ -357,6 +402,7 @@ class TestPhotoUpload(TestPhotos): self.assertIn("photo", files) self.assertEqual(result.get_fields(), self.test_photos_dict[0]) +class TestPhotoUploadEncoded(TestPhotos): @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_upload_encoded(self, mock_post): """Check that a photo can be uploaded using Base64 encoding""" @@ -368,6 +414,18 @@ class TestPhotoUpload(TestPhotos): photo=encoded_file, title="Test") self.assertEqual(result.get_fields(), self.test_photos_dict[0]) +class TestPhotoUploadFromUrl(TestPhotos): + @mock.patch.object(trovebox.Trovebox, 'post') + def test_photo_upload_from_url(self, mock_post): + """ + Check that a photo can be imported from a url. + """ + mock_post.return_value = self._return_value(self.test_photos_dict[0]) + result = self.client.photo.upload_from_url("test_url", title="Test") + mock_post.assert_called_with("/photo/upload.json", + photo="test_url", title="Test") + self.assertEqual(result.get_fields(), self.test_photos_dict[0]) + class TestPhotoDynamicUrl(TestPhotos): @mock.patch.object(trovebox.Trovebox, 'get') def test_photo_dynamic_url(self, _): diff --git a/trovebox/api/api_photo.py b/trovebox/api/api_photo.py index bffb0cb..c9c2c73 100644 --- a/trovebox/api/api_photo.py +++ b/trovebox/api/api_photo.py @@ -110,7 +110,19 @@ class ApiPhoto(ApiBase): **kwds)["result"] return Photo(self._client, result) -# def replace_from_url(self, url, **kwds): + def replace_from_url(self, photo, url, **kwds): + """ + Endpoint: /photo/replace.json + + Import a photo from the specified URL to replace an existing + photo. + """ + result = self._client.post("/photo/%s/replace.json" % + self._extract_id(photo), + photo=url, + **kwds)["result"] + return Photo(self._client, result) + def update(self, photo, **kwds): """ @@ -163,7 +175,15 @@ class ApiPhoto(ApiBase): **kwds)["result"] return Photo(self._client, result) -# def upload_from_url(self, url, **kwds): + def upload_from_url(self, url, **kwds): + """ + Endpoint: /photo/upload.json + + Import a photo from the specified URL + """ + result = self._client.post("/photo/upload.json", photo=url, + **kwds)["result"] + return Photo(self._client, result) def dynamic_url(self, photo, **kwds): """ Not yet implemented """ diff --git a/trovebox/objects/photo.py b/trovebox/objects/photo.py index 89aa769..479f430 100644 --- a/trovebox/objects/photo.py +++ b/trovebox/objects/photo.py @@ -49,6 +49,15 @@ class Photo(TroveboxObject): **kwds) self._replace_fields(result.get_fields()) + def replace_from_url(self, url, **kwds): + """ + Endpoint: /photo/replace.json + + Import a photo from the specified URL to replace this photo. + """ + result = self._client.photo.replace_from_url(self, url, **kwds) + self._replace_fields(result.get_fields()) + def update(self, **kwds): """ Endpoint: /photo//update.json From 25f8a2cefd1acc19e3de76dc28badf52353f75e6 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Thu, 12 Sep 2013 19:56:12 +0100 Subject: [PATCH 57/80] Introduce a small delay between functional tests, to allow CTRL-C abort --- run_functional_tests | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/run_functional_tests b/run_functional_tests index c918073..896e413 100755 --- a/run_functional_tests +++ b/run_functional_tests @@ -9,6 +9,7 @@ tput setaf 3 echo echo "Testing latest self-hosted site..." tput sgr0 +sleep 1 export TROVEBOX_TEST_CONFIG=test unset TROVEBOX_TEST_SERVER_API python -m unittest discover --catch tests/functional @@ -19,6 +20,7 @@ tput setaf 3 echo echo "Testing APIv1 self-hosted site..." tput sgr0 +sleep 1 export TROVEBOX_TEST_CONFIG=test-apiv1 export TROVEBOX_TEST_SERVER_API=1 python -m unittest discover --catch tests/functional @@ -29,6 +31,7 @@ tput setaf 3 echo echo "Testing v3.0.8 self-hosted site..." tput sgr0 +sleep 1 export TROVEBOX_TEST_CONFIG=test-3.0.8 unset TROVEBOX_TEST_SERVER_API python -m unittest discover --catch tests/functional @@ -38,6 +41,7 @@ tput setaf 3 echo echo "Testing latest hosted site..." tput sgr0 +sleep 1 export TROVEBOX_TEST_CONFIG=test-hosted unset TROVEBOX_TEST_SERVER_API python -m unittest discover --catch tests/functional @@ -47,6 +51,7 @@ tput setaf 3 echo echo "Testing latest hosted site over HTTPS..." tput sgr0 +sleep 1 export TROVEBOX_TEST_CONFIG=test-hosted-https unset TROVEBOX_TEST_SERVER_API python -m unittest discover --catch tests/functional From 9721fb28bcf65f96d3612154f9b0e6f1a46730b7 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Thu, 12 Sep 2013 19:56:54 +0100 Subject: [PATCH 58/80] Use requests to download photos, to prevent URL problems during API version testing --- tests/functional/test_photos.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/functional/test_photos.py b/tests/functional/test_photos.py index 7b8899d..30e3c98 100644 --- a/tests/functional/test_photos.py +++ b/tests/functional/test_photos.py @@ -5,6 +5,7 @@ try: except ImportError: import unittest +import requests import trovebox from tests.functional import test_base @@ -76,16 +77,16 @@ class TestPhotos(test_base.TestBase): def test_delete_source(self): """ Test that photo source files can be deleted """ - # Upload a new (duplicate) photo + # Upload a new (duplicate) public photo photo = self.client.photo.upload("tests/data/test_photo1.jpg", - allowDuplicate=True) + allowDuplicate=True, + permission=True) # Check that the photo can be downloaded - self.client.get("photo/%s/download" % photo.id, process_response=False) + self.assertEqual(requests.get(photo.pathDownload).status_code, 200) # Delete the source and check that the source file no longer exists photo.delete_source() - with self.assertRaises(trovebox.TroveboxError): - self.client.get("photo/%s/download" % photo.id, process_response=False) + self.assertEqual(requests.get(photo.pathDownload).status_code, 404) # Put the environment back the way we found it photo.delete() From 379f3329328adf4d36145b977a6b234ade5a9ce9 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 14 Sep 2013 11:52:17 +0100 Subject: [PATCH 59/80] Remove APIv1 test server from functional tests. There are too many exceptions required, and this is now very old. --- run_functional_tests | 11 ----------- tests/functional/README.markdown | 1 - 2 files changed, 12 deletions(-) diff --git a/run_functional_tests b/run_functional_tests index 896e413..35d58a3 100755 --- a/run_functional_tests +++ b/run_functional_tests @@ -14,17 +14,6 @@ export TROVEBOX_TEST_CONFIG=test unset TROVEBOX_TEST_SERVER_API python -m unittest discover --catch tests/functional -# Test server running APIv1 Trovebox instance -# Install from photo/frontend commit 660b2ab -tput setaf 3 -echo -echo "Testing APIv1 self-hosted site..." -tput sgr0 -sleep 1 -export TROVEBOX_TEST_CONFIG=test-apiv1 -export TROVEBOX_TEST_SERVER_API=1 -python -m unittest discover --catch tests/functional - # Test server running v3.0.8 Trovebox instance # Install from photo/frontend commit e9d81de57b tput setaf 3 diff --git a/tests/functional/README.markdown b/tests/functional/README.markdown index 6a20718..734a2a4 100644 --- a/tests/functional/README.markdown +++ b/tests/functional/README.markdown @@ -100,7 +100,6 @@ To use it, you must set up multiple Trovebox instances and create the following config files containing your credentials: test : Latest self-hosted site (from photo/frontend master branch) - test-apiv1 : APIv1 self-hosted site (from photo/frontend commit 660b2ab) test-3.0.8 : v3.0.8 self-hosted site (from photo/frontend commit e9d81de57b) test-hosted : Credentials for test account on http://.trovebox.com test-hosted-https : Same as test-hosted, but with https:// From 00efc91ef97870d6c1c3d00c3fd2d5c4aa92588f Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 14 Sep 2013 11:53:11 +0100 Subject: [PATCH 60/80] Use pathOriginal for upload/delete tests. Hosted site returns 403 not 404 for a missing source photo. --- tests/functional/test_photos.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_photos.py b/tests/functional/test_photos.py index 30e3c98..edb4978 100644 --- a/tests/functional/test_photos.py +++ b/tests/functional/test_photos.py @@ -82,11 +82,12 @@ class TestPhotos(test_base.TestBase): allowDuplicate=True, permission=True) # Check that the photo can be downloaded - self.assertEqual(requests.get(photo.pathDownload).status_code, 200) + self.assertEqual(requests.get(photo.pathOriginal).status_code, 200) # Delete the source and check that the source file no longer exists photo.delete_source() - self.assertEqual(requests.get(photo.pathDownload).status_code, 404) + self.assertIn(requests.get(photo.pathOriginal).status_code, + [403, 404]) # Put the environment back the way we found it photo.delete() @@ -107,7 +108,7 @@ class TestPhotos(test_base.TestBase): # Make an existing photo public self.photos[0].update(permission=True) # Upload a duplicate of an existing photo - self.client.photo.upload_from_url(self.photos[0].pathDownload, + self.client.photo.upload_from_url(self.photos[0].pathOriginal, allowDuplicate=True) # Check there are now four photos photos = self.client.photos.list() From 3b855794e7992206428c624306802c62c18ef181 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 14 Sep 2013 11:53:44 +0100 Subject: [PATCH 61/80] Add pylintrc to supress "locally-disabled" messages --- pylintrc | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 pylintrc diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..e417055 --- /dev/null +++ b/pylintrc @@ -0,0 +1,16 @@ +[MESSAGES CONTROL] +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=locally-disabled From 8a4a806f8d183f6010d15f73dd90616b5b7e747f Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 14 Sep 2013 12:00:28 +0100 Subject: [PATCH 62/80] Fixed typo in test definition --- tests/unit/test_photos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index 349843a..f7b42ae 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -68,7 +68,7 @@ class TestPhotosList(TestPhotos): ("/photos/test1-test2/foo-bar/list.json",)]) self.assertEqual(mock_get.call_args[1], {"foo": "bar"}) -class TestPhotosList(TestPhotos): +class TestPhotosShare(TestPhotos): @mock.patch.object(trovebox.Trovebox, 'post') def test_photos_share(self, mock_post): self.client.photos.share(filters={"foo": "bar", From 5ccc03a835cfda9aa70bc5305ffaa26172eacbaa Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 14 Sep 2013 12:18:38 +0100 Subject: [PATCH 63/80] Add testcases for updating with empty attributes --- tests/unit/test_actions.py | 8 ++++++++ tests/unit/test_activities.py | 9 +++++++++ tests/unit/test_albums.py | 9 +++++++++ 3 files changed, 26 insertions(+) diff --git a/tests/unit/test_actions.py b/tests/unit/test_actions.py index ffb83dc..611ff5e 100644 --- a/tests/unit/test_actions.py +++ b/tests/unit/test_actions.py @@ -141,3 +141,11 @@ class TestActionView(TestActions): self.assertEqual(action.target.id, "photo2") self.assertEqual(action.target_type, "photo") +class TestActionMisc(TestActions): + def test_update_fields_with_no_target(self): + """Check that an action object can be updated with no target""" + action = self.test_actions[0] + action.target = None + action.target_type = None + # Check that no exception is raised + action._update_fields_with_objects() diff --git a/tests/unit/test_activities.py b/tests/unit/test_activities.py index d77f460..df0a7ba 100644 --- a/tests/unit/test_activities.py +++ b/tests/unit/test_activities.py @@ -131,3 +131,12 @@ class TestActivityView(TestActivities): {"data": "", "type": "invalid"})) with self.assertRaises(NotImplementedError): self.client.activity.view(self.test_activities[0]) + +class TestActivityMisc(TestActivities): + def test_update_fields_with_no_type(self): + """Check that an activity object can be updated with no type""" + activity = self.test_activities[0] + activity.type = None + activity.data = None + # Check that no exception is raised + activity._update_fields_with_objects() diff --git a/tests/unit/test_albums.py b/tests/unit/test_albums.py index 811c39f..6bdb0b7 100644 --- a/tests/unit/test_albums.py +++ b/tests/unit/test_albums.py @@ -355,3 +355,12 @@ class TestAlbumView(TestAlbums): self.assertEqual(album.cover.id, "2b") self.assertEqual(album.cover.tags, ["tag3", "tag4"]) self.assertEqual(album.photos[0].id, self.test_photos[1].id) + +class TestAlbumMisc(TestAlbums): + def test_update_fields_with_no_cover(self): + """Check that an album object can be updated with no cover""" + album = self.test_albums[0] + album.cover = None + album.photos = None + # Check that no exception is raised + album._update_fields_with_objects() From 8c96b22b44ef75b859d98d3dc55d9fb193aa8192 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 14 Sep 2013 12:19:08 +0100 Subject: [PATCH 64/80] No coverage required for response logging truncation --- trovebox/http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trovebox/http.py b/trovebox/http.py index 7f6f666..0a99fab 100644 --- a/trovebox/http.py +++ b/trovebox/http.py @@ -105,7 +105,7 @@ class Http(object): self._logger.info("GET %s" % url) self._logger.info("---") self._logger.info(response.text[:1000]) - if len(response.text) > 1000: + if len(response.text) > 1000: # pragma: no cover self._logger.info("[Response truncated to 1000 characters]") self.last_url = url @@ -161,7 +161,7 @@ class Http(object): self._logger.info("files: %s" % repr(files)) self._logger.info("---") self._logger.info(response.text[:1000]) - if len(response.text) > 1000: + if len(response.text) > 1000: # pragma: no cover self._logger.info("[Response truncated to 1000 characters]") self.last_url = url From 42b04b0c892ad2d4f22e2ddb52f1a43c3bd08abf Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 14 Sep 2013 12:19:43 +0100 Subject: [PATCH 65/80] No need to trap AttributeError, since cover will always exist --- trovebox/objects/album.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/trovebox/objects/album.py b/trovebox/objects/album.py index 352891e..7a335cd 100644 --- a/trovebox/objects/album.py +++ b/trovebox/objects/album.py @@ -17,11 +17,8 @@ class Album(TroveboxObject): def _update_fields_with_objects(self): """ Convert dict fields into objects, where appropriate """ # Update the cover with a photo object - try: - if isinstance(self.cover, dict): - self.cover = Photo(self._client, self.cover) - except AttributeError: - pass # No cover + if isinstance(self.cover, dict): + self.cover = Photo(self._client, self.cover) # Update the photo list with photo objects try: From aeb06c0d8bb74a5e9d7f0ea7db8e655a76b769ff Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 15 Sep 2013 08:25:51 +0100 Subject: [PATCH 66/80] Added photo view options parameter Renamed "filter" parameters to "option" --- tests/functional/test_activities.py | 8 +++--- tests/functional/test_photos.py | 12 ++++----- tests/unit/test_activities.py | 6 ++--- tests/unit/test_photos.py | 38 ++++++++++++++++++++++------- trovebox/api/api_activity.py | 12 ++++----- trovebox/api/api_base.py | 16 ++++++------ trovebox/api/api_photo.py | 32 ++++++++++++------------ 7 files changed, 73 insertions(+), 51 deletions(-) diff --git a/tests/functional/test_activities.py b/tests/functional/test_activities.py index 854e70a..43e3af4 100644 --- a/tests/functional/test_activities.py +++ b/tests/functional/test_activities.py @@ -30,9 +30,9 @@ class TestActivities(test_base.TestBase): for photo in photos: photo.update(tags=self.TEST_TAG) - def test_list_filter(self): + def test_list_option(self): """ - Check that the activity list filter parameter works correctly + Check that the activity list options parameter works correctly """ self._delete_all() self._create_test_photos(tag=False) @@ -42,8 +42,8 @@ class TestActivities(test_base.TestBase): photos[0].update(tags=photos[0].tags) # Check that the activities can be filtered - upload_activities = self.client.activities.list(filters={"type": "photo-upload"}) - update_activities = self.client.activities.list(filters={"type": "photo-update"}) + upload_activities = self.client.activities.list(options={"type": "photo-upload"}) + update_activities = self.client.activities.list(options={"type": "photo-update"}) self.assertEqual(len(upload_activities), len(photos)) self.assertEqual(len(update_activities), 1) diff --git a/tests/functional/test_photos.py b/tests/functional/test_photos.py index edb4978..d00c689 100644 --- a/tests/functional/test_photos.py +++ b/tests/functional/test_photos.py @@ -12,21 +12,21 @@ from tests.functional import test_base class TestPhotos(test_base.TestBase): testcase_name = "photo API" - def test_list_filter(self): + def test_list_option(self): """ - Check that the photo list filter parameter works correctly + Check that the photo list options parameter works correctly """ - filter_tag = "Filter" + option_tag = "Filter" # Assign a photo with a new tag - self.photos[0].update(tagsAdd=filter_tag) + self.photos[0].update(tagsAdd=option_tag) # Check that the photos can be filtered - photos = self.client.photos.list(filters={"tags": filter_tag}) + photos = self.client.photos.list(options={"tags": option_tag}) self.assertEqual(len(photos), 1) self.assertEqual(photos[0].id, self.photos[0].id) # Put the environment back the way we found it - photos[0].update(tagsRemove=filter_tag) + photos[0].update(tagsRemove=option_tag) # Photo share endpoint is currently not implemented @unittest.expectedFailure diff --git a/tests/unit/test_activities.py b/tests/unit/test_activities.py index df0a7ba..9d209ae 100644 --- a/tests/unit/test_activities.py +++ b/tests/unit/test_activities.py @@ -69,10 +69,10 @@ class TestActivitiesList(TestActivities): self.assertEqual(result, []) @mock.patch.object(trovebox.Trovebox, 'get') - def test_filters(self, mock_get): - """Check that the activity list filters are applied properly""" + def test_options(self, mock_get): + """Check that the activity list optionss are applied properly""" mock_get.return_value = self._return_value(self.test_activities_dict) - self.client.activities.list(filters={"foo": "bar", + self.client.activities.list(options={"foo": "bar", "test1": "test2"}, foo="bar") # Dict element can be any order diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index f7b42ae..2a739fe 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -56,10 +56,10 @@ class TestPhotosList(TestPhotos): self.assertEqual(result, []) @mock.patch.object(trovebox.Trovebox, 'get') - def test_filters(self, mock_get): - """Check that the activity list filters are applied properly""" + def test_options(self, mock_get): + """Check that the activity list options are applied properly""" mock_get.return_value = self._return_value(self.test_photos_dict) - self.client.photos.list(filters={"foo": "bar", + self.client.photos.list(options={"foo": "bar", "test1": "test2"}, foo="bar") # Dict element can be any order @@ -71,7 +71,7 @@ class TestPhotosList(TestPhotos): class TestPhotosShare(TestPhotos): @mock.patch.object(trovebox.Trovebox, 'post') def test_photos_share(self, mock_post): - self.client.photos.share(filters={"foo": "bar", + self.client.photos.share(options={"foo": "bar", "test1": "test2"}, foo="bar") # Dict element can be any order @@ -362,16 +362,30 @@ class TestPhotoView(TestPhotos): """Check that a photo can be viewed""" mock_get.return_value = self._return_value(self.test_photos_dict[1]) result = self.client.photo.view(self.test_photos[0], + options={"foo": "bar", + "test1": "test2"}, returnSizes="20x20") - mock_get.assert_called_with("/photo/1a/view.json", returnSizes="20x20") + # Dict elemet can be in any order + self.assertIn(mock_get.call_args[0], + [("/photo/1a/foo-bar/test1-test2/view.json",), + ("/photo/1a/test1-test2/foo-bar/view.json",)]) + self.assertEqual(mock_get.call_args[1], {"returnSizes": "20x20"}) self.assertEqual(result.get_fields(), self.test_photos_dict[1]) @mock.patch.object(trovebox.Trovebox, 'get') def test_photo_view_id(self, mock_get): """Check that a photo can be viewed using its ID""" mock_get.return_value = self._return_value(self.test_photos_dict[1]) - result = self.client.photo.view("1a", returnSizes="20x20") - mock_get.assert_called_with("/photo/1a/view.json", returnSizes="20x20") + result = self.client.photo.view("1a", + options={"foo": "bar", + "test1": "test2"}, + returnSizes="20x20") + + # Dict elemet can be in any order + self.assertIn(mock_get.call_args[0], + [("/photo/1a/foo-bar/test1-test2/view.json",), + ("/photo/1a/test1-test2/foo-bar/view.json",)]) + self.assertEqual(mock_get.call_args[1], {"returnSizes": "20x20"}) self.assertEqual(result.get_fields(), self.test_photos_dict[1]) @mock.patch.object(trovebox.Trovebox, 'get') @@ -382,8 +396,14 @@ class TestPhotoView(TestPhotos): """ mock_get.return_value = self._return_value(self.test_photos_dict[1]) photo = self.test_photos[0] - photo.view(returnSizes="20x20") - mock_get.assert_called_with("/photo/1a/view.json", returnSizes="20x20") + photo.view(returnSizes="20x20", options={"foo": "bar", + "test1": "test2"}) + + # Dict elemet can be in any order + self.assertIn(mock_get.call_args[0], + [("/photo/1a/foo-bar/test1-test2/view.json",), + ("/photo/1a/test1-test2/foo-bar/view.json",)]) + self.assertEqual(mock_get.call_args[1], {"returnSizes": "20x20"}) self.assertEqual(photo.get_fields(), self.test_photos_dict[1]) class TestPhotoUpload(TestPhotos): diff --git a/trovebox/api/api_activity.py b/trovebox/api/api_activity.py index cd8e308..bccbd84 100644 --- a/trovebox/api/api_activity.py +++ b/trovebox/api/api_activity.py @@ -7,16 +7,16 @@ from .api_base import ApiBase class ApiActivities(ApiBase): """ Definitions of /activities/ API endpoints """ - def list(self, filters=None, **kwds): + def list(self, options=None, **kwds): """ - Endpoint: /activities/[]/list.json + Endpoint: /activities/[]/list.json Returns a list of Activity objects. - The filters parameter can be used to narrow down the activities. - Eg: filters={"type": "photo-upload"} + The options parameter can be used to narrow down the activities. + Eg: options={"type": "photo-upload"} """ - filter_string = self._build_filter_string(filters) - activities = self._client.get("/activities/%slist.json" % filter_string, + option_string = self._build_option_string(options) + activities = self._client.get("/activities/%slist.json" % option_string, **kwds)["result"] activities = self._result_to_list(activities) return [Activity(self._client, activity) for activity in activities] diff --git a/trovebox/api/api_base.py b/trovebox/api/api_base.py index 15f0f4b..c79ab9f 100644 --- a/trovebox/api/api_base.py +++ b/trovebox/api/api_base.py @@ -8,16 +8,16 @@ class ApiBase(object): self._client = client @staticmethod - def _build_filter_string(filters): + def _build_option_string(options): """ - :param filters: dictionary containing the filters - :returns: filter_string formatted for an API endpoint + :param options: dictionary containing the options + :returns: option_string formatted for an API endpoint """ - filter_string = "" - if filters is not None: - for filt in filters: - filter_string += "%s-%s/" % (filt, filters[filt]) - return filter_string + option_string = "" + if options is not None: + for key in options: + option_string += "%s-%s/" % (key, options[key]) + return option_string @staticmethod def _extract_id(obj): diff --git a/trovebox/api/api_photo.py b/trovebox/api/api_photo.py index c9c2c73..f9a86bb 100644 --- a/trovebox/api/api_photo.py +++ b/trovebox/api/api_photo.py @@ -8,28 +8,28 @@ from .api_base import ApiBase class ApiPhotos(ApiBase): """ Definitions of /photos/ API endpoints """ - def list(self, filters=None, **kwds): + def list(self, options=None, **kwds): """ - Endpoint: /photos/[]/list.json + Endpoint: /photos/[]/list.json Returns a list of Photo objects. - The filters parameter can be used to narrow down the list. - Eg: filters={"album": } + The options parameter can be used to narrow down the list. + Eg: options={"album": } """ - filter_string = self._build_filter_string(filters) - photos = self._client.get("/photos/%slist.json" % filter_string, + option_string = self._build_option_string(options) + photos = self._client.get("/photos/%slist.json" % option_string, **kwds)["result"] photos = self._result_to_list(photos) return [Photo(self._client, photo) for photo in photos] - def share(self, filters=None, **kwds): + def share(self, options=None, **kwds): """ - Endpoint: /photos/[/share.json + Endpoint: /photos/[/share.json Not currently implemented. """ - filter_string = self._build_filter_string(filters) - return self._client.post("/photos/%sshare.json" % filter_string, + option_string = self._build_option_string(options) + return self._client.post("/photos/%sshare.json" % option_string, **kwds)["result"] def delete(self, photos, **kwds): @@ -136,18 +136,20 @@ class ApiPhoto(ApiBase): **kwds)["result"] return Photo(self._client, result) - # TODO: Add options - def view(self, photo, **kwds): + def view(self, photo, options=None, **kwds): """ - Endpoint: /photo//view.json + Endpoint: /photo//[]/view.json Requests all properties of a photo. Can be used to obtain URLs for the photo at a particular size, by using the "returnSizes" parameter. Returns the requested photo object. + The options parameter can be used to pass in additional options. + Eg: options={"token": } """ - result = self._client.get("/photo/%s/view.json" % - self._extract_id(photo), + option_string = self._build_option_string(options) + result = self._client.get("/photo/%s/%sview.json" % + (self._extract_id(photo), option_string), **kwds)["result"] return Photo(self._client, result) From 9507abdd08d99a47a84de338439b94b1161d3382 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 15 Sep 2013 08:27:46 +0100 Subject: [PATCH 67/80] Add options parameter to photo object view method --- trovebox/objects/photo.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/trovebox/objects/photo.py b/trovebox/objects/photo.py index 479f430..e4a2c0d 100644 --- a/trovebox/objects/photo.py +++ b/trovebox/objects/photo.py @@ -67,8 +67,7 @@ class Photo(TroveboxObject): result = self._client.photo.update(self, **kwds) self._replace_fields(result.get_fields()) - # TODO: Add options - def view(self, **kwds): + def view(self, options=None, **kwds): """ Endpoint: /photo//view.json @@ -76,8 +75,10 @@ class Photo(TroveboxObject): Can be used to obtain URLs for the photo at a particular size, by using the "returnSizes" parameter. Updates the photo's fields with the response. + The options parameter can be used to pass in additional options. + Eg: options={"token": } """ - result = self._client.photo.view(self, **kwds) + result = self._client.photo.view(self, options, **kwds) self._replace_fields(result.get_fields()) def dynamic_url(self, **kwds): From 3a13d13c424756ba7f971709f8732a65d9577ab7 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 15 Sep 2013 08:50:26 +0100 Subject: [PATCH 68/80] Added photo next_previous option argument. Moved options_string slash to the start of the string. --- tests/unit/test_photos.py | 32 ++++++++++++++++++++++++-------- trovebox/api/api_activity.py | 4 ++-- trovebox/api/api_base.py | 2 +- trovebox/api/api_photo.py | 23 +++++++++++++---------- trovebox/objects/photo.py | 9 ++++----- 5 files changed, 44 insertions(+), 26 deletions(-) diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index 2a739fe..280e60d 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -473,9 +473,14 @@ class TestPhotoNextPrevious(TestPhotos): {"next": [self.test_photos_dict[0]], "previous": [self.test_photos_dict[1]]}) result = self.client.photo.next_previous(self.test_photos[0], + options={"foo": "bar", + "test1": "test2"}, foo="bar") - mock_get.assert_called_with("/photo/1a/nextprevious.json", - foo="bar") + # Dict elemet can be in any order + self.assertIn(mock_get.call_args[0], + [("/photo/1a/nextprevious/foo-bar/test1-test2.json",), + ("/photo/1a/nextprevious/test1-test2/foo-bar.json",)]) + self.assertEqual(mock_get.call_args[1], {"foo": "bar"}) self.assertEqual(result["next"][0].get_fields(), self.test_photos_dict[0]) self.assertEqual(result["previous"][0].get_fields(), @@ -490,9 +495,15 @@ class TestPhotoNextPrevious(TestPhotos): mock_get.return_value = self._return_value( {"next": [self.test_photos_dict[0]], "previous": [self.test_photos_dict[1]]}) - result = self.client.photo.next_previous("1a", foo="bar") - mock_get.assert_called_with("/photo/1a/nextprevious.json", - foo="bar") + result = self.client.photo.next_previous("1a", + options={"foo": "bar", + "test1": "test2"}, + foo="bar") + # Dict elemet can be in any order + self.assertIn(mock_get.call_args[0], + [("/photo/1a/nextprevious/foo-bar/test1-test2.json",), + ("/photo/1a/nextprevious/test1-test2/foo-bar.json",)]) + self.assertEqual(mock_get.call_args[1], {"foo": "bar"}) self.assertEqual(result["next"][0].get_fields(), self.test_photos_dict[0]) self.assertEqual(result["previous"][0].get_fields(), @@ -507,9 +518,14 @@ class TestPhotoNextPrevious(TestPhotos): mock_get.return_value = self._return_value( {"next": [self.test_photos_dict[0]], "previous": [self.test_photos_dict[1]]}) - result = self.test_photos[0].next_previous(foo="bar") - mock_get.assert_called_with("/photo/1a/nextprevious.json", - foo="bar") + result = self.test_photos[0].next_previous(options={"foo": "bar", + "test1": "test2"}, + foo="bar") + # Dict elemet can be in any order + self.assertIn(mock_get.call_args[0], + [("/photo/1a/nextprevious/foo-bar/test1-test2.json",), + ("/photo/1a/nextprevious/test1-test2/foo-bar.json",)]) + self.assertEqual(mock_get.call_args[1], {"foo": "bar"}) self.assertEqual(result["next"][0].get_fields(), self.test_photos_dict[0]) self.assertEqual(result["previous"][0].get_fields(), diff --git a/trovebox/api/api_activity.py b/trovebox/api/api_activity.py index bccbd84..2e17c88 100644 --- a/trovebox/api/api_activity.py +++ b/trovebox/api/api_activity.py @@ -9,14 +9,14 @@ class ApiActivities(ApiBase): """ Definitions of /activities/ API endpoints """ def list(self, options=None, **kwds): """ - Endpoint: /activities/[]/list.json + Endpoint: /activities[/]/list.json Returns a list of Activity objects. The options parameter can be used to narrow down the activities. Eg: options={"type": "photo-upload"} """ option_string = self._build_option_string(options) - activities = self._client.get("/activities/%slist.json" % option_string, + activities = self._client.get("/activities%s/list.json" % option_string, **kwds)["result"] activities = self._result_to_list(activities) return [Activity(self._client, activity) for activity in activities] diff --git a/trovebox/api/api_base.py b/trovebox/api/api_base.py index c79ab9f..5aaf16c 100644 --- a/trovebox/api/api_base.py +++ b/trovebox/api/api_base.py @@ -16,7 +16,7 @@ class ApiBase(object): option_string = "" if options is not None: for key in options: - option_string += "%s-%s/" % (key, options[key]) + option_string += "/%s-%s" % (key, options[key]) return option_string @staticmethod diff --git a/trovebox/api/api_photo.py b/trovebox/api/api_photo.py index f9a86bb..caa67e5 100644 --- a/trovebox/api/api_photo.py +++ b/trovebox/api/api_photo.py @@ -10,26 +10,26 @@ class ApiPhotos(ApiBase): """ Definitions of /photos/ API endpoints """ def list(self, options=None, **kwds): """ - Endpoint: /photos/[]/list.json + Endpoint: /photos[/]/list.json Returns a list of Photo objects. The options parameter can be used to narrow down the list. Eg: options={"album": } """ option_string = self._build_option_string(options) - photos = self._client.get("/photos/%slist.json" % option_string, + photos = self._client.get("/photos%s/list.json" % option_string, **kwds)["result"] photos = self._result_to_list(photos) return [Photo(self._client, photo) for photo in photos] def share(self, options=None, **kwds): """ - Endpoint: /photos/[/share.json + Endpoint: /photos[//share.json Not currently implemented. """ option_string = self._build_option_string(options) - return self._client.post("/photos/%sshare.json" % option_string, + return self._client.post("/photos%s/share.json" % option_string, **kwds)["result"] def delete(self, photos, **kwds): @@ -138,7 +138,7 @@ class ApiPhoto(ApiBase): def view(self, photo, options=None, **kwds): """ - Endpoint: /photo//[]/view.json + Endpoint: /photo/[/]/view.json Requests all properties of a photo. Can be used to obtain URLs for the photo at a particular size, @@ -148,7 +148,7 @@ class ApiPhoto(ApiBase): Eg: options={"token": } """ option_string = self._build_option_string(options) - result = self._client.get("/photo/%s/%sview.json" % + result = self._client.get("/photo/%s%s/view.json" % (self._extract_id(photo), option_string), **kwds)["result"] return Photo(self._client, result) @@ -192,15 +192,18 @@ class ApiPhoto(ApiBase): raise NotImplementedError() # TODO: Add options - def next_previous(self, photo, **kwds): + def next_previous(self, photo, options=None, **kwds): """ - Endpoint: /photo//nextprevious.json + Endpoint: /photo//nextprevious[/].json Returns a dict containing the next and previous photo lists (there may be more than one next/previous photo returned). + The options parameter can be used to narrow down the photos + Eg: options={"album": } """ - result = self._client.get("/photo/%s/nextprevious.json" % - self._extract_id(photo), + option_string = self._build_option_string(options) + result = self._client.get("/photo/%s/nextprevious%s.json" % + (self._extract_id(photo), option_string), **kwds)["result"] value = {} if "next" in result: diff --git a/trovebox/objects/photo.py b/trovebox/objects/photo.py index e4a2c0d..af301a4 100644 --- a/trovebox/objects/photo.py +++ b/trovebox/objects/photo.py @@ -69,7 +69,7 @@ class Photo(TroveboxObject): def view(self, options=None, **kwds): """ - Endpoint: /photo//view.json + Endpoint: /photo/[/]/view.json Requests all properties of this photo. Can be used to obtain URLs for the photo at a particular size, @@ -85,15 +85,14 @@ class Photo(TroveboxObject): """ Not implemented yet """ raise NotImplementedError() - # TODO: Add options - def next_previous(self, **kwds): + def next_previous(self, options=None, **kwds): """ - Endpoint: /photo//nextprevious.json + Endpoint: /photo//nextprevious[/].json Returns a dict containing the next and previous photo lists (there may be more than one next/previous photo returned). """ - return self._client.photo.next_previous(self, **kwds) + return self._client.photo.next_previous(self, options, **kwds) def transform(self, **kwds): """ From ebaf3bc0361b979f08eec3fbbfe69ab0ce872a69 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 15 Sep 2013 11:28:40 +0100 Subject: [PATCH 69/80] Remove photo dynamic_url, since this is marked as "internal" in the frontend code --- tests/functional/test_photos.py | 5 ----- tests/unit/test_photos.py | 19 ------------------- trovebox/api/api_photo.py | 5 ----- trovebox/objects/photo.py | 4 ---- 4 files changed, 33 deletions(-) diff --git a/tests/functional/test_photos.py b/tests/functional/test_photos.py index d00c689..8b561c4 100644 --- a/tests/functional/test_photos.py +++ b/tests/functional/test_photos.py @@ -202,11 +202,6 @@ class TestPhotos(test_base.TestBase): allowDuplicate=True) self.assertEqual(self.photos[0].hash, original_hash) - def test_dynamic_url(self): - """ If photo.dynamic_url gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.client.photo.dynamic_url(None) - def test_transform(self): """ Test photo rotation """ photo = self.photos[0] diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index 280e60d..c4ee610 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -446,25 +446,6 @@ class TestPhotoUploadFromUrl(TestPhotos): photo="test_url", title="Test") self.assertEqual(result.get_fields(), self.test_photos_dict[0]) -class TestPhotoDynamicUrl(TestPhotos): - @mock.patch.object(trovebox.Trovebox, 'get') - def test_photo_dynamic_url(self, _): - """ If photo.dynamic_url gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.client.photo.dynamic_url(self.test_photos[0]) - - @mock.patch.object(trovebox.Trovebox, 'get') - def test_photo_dynamic_url_id(self, _): - """ If photo.dynamic_url gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.client.photo.dynamic_url("1a") - - @mock.patch.object(trovebox.Trovebox, 'get') - def test_photo_object_dynamic_url(self, _): - """ If photo.dynamic_url gets implemented, write a test! """ - with self.assertRaises(NotImplementedError): - self.test_photos[0].dynamic_url() - class TestPhotoNextPrevious(TestPhotos): @mock.patch.object(trovebox.Trovebox, 'get') def test_photo_next_previous(self, mock_get): diff --git a/trovebox/api/api_photo.py b/trovebox/api/api_photo.py index caa67e5..dfc98ff 100644 --- a/trovebox/api/api_photo.py +++ b/trovebox/api/api_photo.py @@ -187,11 +187,6 @@ class ApiPhoto(ApiBase): **kwds)["result"] return Photo(self._client, result) - def dynamic_url(self, photo, **kwds): - """ Not yet implemented """ - raise NotImplementedError() - - # TODO: Add options def next_previous(self, photo, options=None, **kwds): """ Endpoint: /photo//nextprevious[/].json diff --git a/trovebox/objects/photo.py b/trovebox/objects/photo.py index af301a4..d2aaf19 100644 --- a/trovebox/objects/photo.py +++ b/trovebox/objects/photo.py @@ -81,10 +81,6 @@ class Photo(TroveboxObject): result = self._client.photo.view(self, options, **kwds) self._replace_fields(result.get_fields()) - def dynamic_url(self, **kwds): - """ Not implemented yet """ - raise NotImplementedError() - def next_previous(self, options=None, **kwds): """ Endpoint: /photo//nextprevious[/].json From 3a6305b4538933c4d3ef9c28089317c580dfab8f Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Wed, 30 Oct 2013 19:33:45 +0000 Subject: [PATCH 70/80] Update test_next_previous to use "dateTaken,asc" sorting --- tests/functional/test_photos.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_photos.py b/tests/functional/test_photos.py index f153b49..55689f4 100644 --- a/tests/functional/test_photos.py +++ b/tests/functional/test_photos.py @@ -127,12 +127,13 @@ class TestPhotos(test_base.TestBase): def test_next_previous(self): """ Test the next/previous links of the middle photo """ - next_prev = self.client.photo.next_previous(self.photos[1]) + next_prev = self.client.photo.next_previous(self.photos[1], + sortBy="dateTaken,asc") self.assertEqual(next_prev["previous"][0].id, self.photos[0].id) self.assertEqual(next_prev["next"][0].id, self.photos[2].id) # Do the same using the Photo object directly - next_prev = self.photos[1].next_previous() + next_prev = self.photos[1].next_previous(sortBy="dateTaken,asc") self.assertEqual(next_prev["previous"][0].id, self.photos[0].id) self.assertEqual(next_prev["next"][0].id, self.photos[2].id) From 09203ff2b8974f4843203082c8908d9675acf2e1 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 3 Nov 2013 12:37:31 +0000 Subject: [PATCH 71/80] Added system endpoints --- tests/unit/test_system.py | 70 ++++++++++++++++++++++++++++++++++++++ trovebox/__init__.py | 2 ++ trovebox/api/api_system.py | 27 +++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 tests/unit/test_system.py create mode 100644 trovebox/api/api_system.py diff --git a/tests/unit/test_system.py b/tests/unit/test_system.py new file mode 100644 index 0000000..7cbdbec --- /dev/null +++ b/tests/unit/test_system.py @@ -0,0 +1,70 @@ +from __future__ import unicode_literals +import json +import httpretty +from httpretty import GET + +# TEMP: Temporary hack until httpretty string checking is fixed +if httpretty.compat.PY3: + httpretty.core.basestring = (bytes, str) + +try: + import unittest2 as unittest # Python2.6 +except ImportError: + import unittest + +import trovebox + +class TestSystem(unittest.TestCase): + test_host = "test.example.com" + + def setUp(self): + self.client = trovebox.Trovebox(host=self.test_host) + + @staticmethod + def _return_value(result, message="", code=200): + return json.dumps({"message": message, "code": code, "result": result}) + +class TestSystemVersion(TestSystem): + test_result = {"api": "v2", + "database": "2.0.0"} + + @httpretty.activate + def test_version(self): + """Check that the version dictionary is returned correctly""" + httpretty.register_uri(GET, uri="http://test.example.com/system/version.json", + body=self._return_value(self.test_result), + status=200) + response = self.client.system.version() + + self.assertEqual(response, self.test_result) + +class TestSystemDiagnostics(TestSystem): + test_result = {'database': [{'label': 'failure', + 'message': 'Could not properly connect to the database.', + 'status': False}], + } + + @httpretty.activate + def test_diagnostics_pass(self): + """Check that the diagnostics dictionary is returned correctly on success""" + httpretty.register_uri(GET, uri="http://test.example.com/system/diagnostics.json", + body=self._return_value(self.test_result), + status=200) + response = self.client.system.diagnostics() + + self.assertEqual(response, self.test_result) + + @httpretty.activate + def test_diagnostics_fail(self): + """ + Check that the diagnostics dictionary is returned correctly on failure. + Although the JSON code is 500, no exception should be raised. + """ + # On failure, the diagnostics endpoint returns a JSON code of 500 + # and a response status code of 200. + httpretty.register_uri(GET, uri="http://test.example.com/system/diagnostics.json", + body=self._return_value(self.test_result, code=500), + status=200) + response = self.client.system.diagnostics() + + self.assertEqual(response, self.test_result) diff --git a/trovebox/__init__.py b/trovebox/__init__.py index 73e2af5..e49df85 100644 --- a/trovebox/__init__.py +++ b/trovebox/__init__.py @@ -9,6 +9,7 @@ from trovebox.api import api_tag from trovebox.api import api_album from trovebox.api import api_action from trovebox.api import api_activity +from trovebox.api import api_system LATEST_API_VERSION = 2 @@ -41,3 +42,4 @@ class Trovebox(Http): self.action = api_action.ApiAction(self) self.activities = api_activity.ApiActivities(self) self.activity = api_activity.ApiActivity(self) + self.system = api_system.ApiSystem(self) diff --git a/trovebox/api/api_system.py b/trovebox/api/api_system.py new file mode 100644 index 0000000..eef087d --- /dev/null +++ b/trovebox/api/api_system.py @@ -0,0 +1,27 @@ +""" +api_system.py : Trovebox System API Classes +""" +from .api_base import ApiBase + +class ApiSystem(ApiBase): + """ Definitions of /system/ API endpoints """ + def version(self, **kwds): + """ + Endpoint: /system/version.json + + Returns a dictionary containing the various server version strings + """ + return self._client.get("/system/version.json", **kwds)["result"] + + def diagnostics(self, **kwds): + """ + Endpoint: /system/diagnostics.json + + Runs a set of diagnostic tests on the server. + Returns a dictionary containing the results. + """ + # Don't process the result automatically, since this raises an exception + # on failure, which doesn't provide the cause of the failure + self._client.get("/system/diagnostics.json", process_response=False, + **kwds) + return self._client.last_response.json()["result"] From be7463518b96de593d2bd0c2832d80d79cc4d560 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 3 Nov 2013 12:38:48 +0000 Subject: [PATCH 72/80] Update httpretty to 0.6.5 (but no further due to https://github.com/gabrielfalcao/HTTPretty/issues/114) --- tests/unit/test_http_errors.py | 4 ---- tests/unit/test_system.py | 4 ---- tox.ini | 13 ++++++++----- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/tests/unit/test_http_errors.py b/tests/unit/test_http_errors.py index f2a7893..994db75 100644 --- a/tests/unit/test_http_errors.py +++ b/tests/unit/test_http_errors.py @@ -4,10 +4,6 @@ import httpretty from httpretty import GET, POST from ddt import ddt, data -# TEMP: Temporary hack until httpretty string checking is fixed -if httpretty.compat.PY3: - httpretty.core.basestring = (bytes, str) - try: import unittest2 as unittest # Python2.6 except ImportError: diff --git a/tests/unit/test_system.py b/tests/unit/test_system.py index 7cbdbec..d505d2b 100644 --- a/tests/unit/test_system.py +++ b/tests/unit/test_system.py @@ -3,10 +3,6 @@ import json import httpretty from httpretty import GET -# TEMP: Temporary hack until httpretty string checking is fixed -if httpretty.compat.PY3: - httpretty.core.basestring = (bytes, str) - try: import unittest2 as unittest # Python2.6 except ImportError: diff --git a/tox.ini b/tox.ini index e552382..7961499 100644 --- a/tox.ini +++ b/tox.ini @@ -2,17 +2,19 @@ envlist = py26, py27, py33, coverage [testenv] -commands = python -m unittest discover --catch tests/unit +commands = python -m unittest discover tests/unit deps = mock >= 1.0.0 - httpretty >= 0.6.1 + # Hold httpretty at 0.6.5 until https://github.com/gabrielfalcao/HTTPretty/issues/114 is resolved + httpretty == 0.6.5 ddt >= 0.3.0 [testenv:py26] -commands = unit2 discover --catch tests/unit +commands = unit2 discover tests/unit deps = mock >= 1.0.0 - httpretty >= 0.6.1 + # Hold httpretty at 0.6.5 until https://github.com/gabrielfalcao/HTTPretty/issues/114 is resolved + httpretty == 0.6.5 ddt >= 0.3.0 unittest2 discover @@ -21,6 +23,7 @@ deps = commands = coverage run --source trovebox setup.py test deps = mock >= 1.0.0 - httpretty >= 0.6.1 + # Hold httpretty at 0.6.5 until https://github.com/gabrielfalcao/HTTPretty/issues/114 is resolved + httpretty == 0.6.5 ddt >= 0.3.0 coverage From 1ba21353f97b13dcd7ed0709f5472b5b84879a59 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Tue, 29 Oct 2013 08:30:38 +0000 Subject: [PATCH 73/80] Don't raise an exception if an object's attribute starts with an underscore, just ignore it. --- tests/unit/test_photos.py | 17 +++++++++++++---- trovebox/objects/trovebox_object.py | 5 ++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index c4ee610..a8c382b 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -606,10 +606,19 @@ class TestPhotoObject(TestPhotos): "name": "Test Name"}) self.assertEqual(repr(photo), "") + def test_photo_object_attribute(self): + """ + Check that attributes are created when creating a + Photo object + """ + photo = trovebox.objects.photo.Photo(self.client, {"attribute": "test"}) + self.assertEqual(photo.attribute, "test") + def test_photo_object_illegal_attribute(self): """ - Check that an exception is raised when creating an Photo object - with an illegal attribute + Check that illegal attributes are ignored when creating a + Photo object """ - with self.assertRaises(ValueError): - photo = trovebox.objects.photo.Photo(self.client, {"_illegal_attribute": "test"}) + photo = trovebox.objects.photo.Photo(self.client, {"_illegal_attribute": "test"}) + with self.assertRaises(AttributeError): + value = photo._illegal_attribute diff --git a/trovebox/objects/trovebox_object.py b/trovebox/objects/trovebox_object.py index b7132bc..20023fa 100644 --- a/trovebox/objects/trovebox_object.py +++ b/trovebox/objects/trovebox_object.py @@ -14,9 +14,8 @@ class TroveboxObject(object): def _set_fields(self, json_dict): """ Set this object's attributes specified in json_dict """ for key, value in json_dict.items(): - if key.startswith("_"): - raise ValueError("Illegal attribute: %s" % key) - setattr(self, key, value) + if not key.startswith("_"): + setattr(self, key, value) def _replace_fields(self, json_dict): """ From c4f152fcc977551a7c75a18b9bca5a5e72f53925 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Tue, 29 Oct 2013 09:30:16 +0000 Subject: [PATCH 74/80] Don't delete/update TroveboxObject attributes that start with an underscore --- tests/unit/test_photos.py | 60 ++++++++++++++++++++++++++++- trovebox/objects/trovebox_object.py | 6 ++- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index a8c382b..9b10a77 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -606,7 +606,8 @@ class TestPhotoObject(TestPhotos): "name": "Test Name"}) self.assertEqual(repr(photo), "") - def test_photo_object_attribute(self): + @mock.patch.object(trovebox.Trovebox, 'post') + def test_photo_object_create_attribute(self, _): """ Check that attributes are created when creating a Photo object @@ -614,11 +615,66 @@ class TestPhotoObject(TestPhotos): photo = trovebox.objects.photo.Photo(self.client, {"attribute": "test"}) self.assertEqual(photo.attribute, "test") - def test_photo_object_illegal_attribute(self): + @mock.patch.object(trovebox.Trovebox, 'post') + def test_photo_object_delete_attribute(self, _): + """ + Check that attributes are deleted when creating a + Photo object + """ + photo = trovebox.objects.photo.Photo(self.client, {"attribute": "test"}) + photo.delete() + with self.assertRaises(AttributeError): + value = photo.attribute + self.assertEqual(photo.get_fields(), {}) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_photo_object_update_attribute(self, mock_post): + """ + Check that attributes are updated when creating a + Photo object + """ + photo = trovebox.objects.photo.Photo(self.client, {"attribute": "test"}) + mock_post.return_value = self._return_value({"attribute": "test2"}) + photo.update() + self.assertEqual(photo.attribute, "test2") + self.assertEqual(photo.get_fields(), {"attribute": "test2"}) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_photo_object_create_illegal_attribute(self, _): """ Check that illegal attributes are ignored when creating a Photo object """ photo = trovebox.objects.photo.Photo(self.client, {"_illegal_attribute": "test"}) + # The object's attribute shouldn't be created with self.assertRaises(AttributeError): value = photo._illegal_attribute + # The field dict gets created correctly, however. + self.assertEqual(photo.get_fields(), {"_illegal_attribute": "test"}) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_photo_object_delete_illegal_attribute(self, _): + """ + Check that illegal attributes are ignored when deleting a + Photo object + """ + photo = trovebox.objects.photo.Photo(self.client, {"_illegal_attribute": "test"}) + photo.delete() + with self.assertRaises(AttributeError): + value = photo._illegal_attribute + self.assertEqual(photo.get_fields(), {}) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_photo_object_update_illegal_attribute(self, mock_post): + """ + Check that illegal attributes are ignored when updating a + Photo object + """ + photo = trovebox.objects.photo.Photo(self.client, {"_illegal_attribute": "test"}) + mock_post.return_value = self._return_value({"_illegal_attribute": "test2"}) + photo.update() + # The object's attribute shouldn't be created + with self.assertRaises(AttributeError): + value = photo._illegal_attribute + # The field dict gets updated correctly, however. + self.assertEqual(photo.get_fields(), {"_illegal_attribute": "test2"}) diff --git a/trovebox/objects/trovebox_object.py b/trovebox/objects/trovebox_object.py index 20023fa..2e02eca 100644 --- a/trovebox/objects/trovebox_object.py +++ b/trovebox/objects/trovebox_object.py @@ -23,7 +23,8 @@ class TroveboxObject(object): those in json_dict. """ for key in self._json_dict.keys(): - delattr(self, key) + if not key.startswith("_"): + delattr(self, key) self._json_dict = json_dict self._set_fields(json_dict) @@ -32,7 +33,8 @@ class TroveboxObject(object): Delete this object's attributes, including name and id """ for key in self._json_dict.keys(): - delattr(self, key) + if not key.startswith("_"): + delattr(self, key) self._json_dict = {} self.id = None self.name = None From 8f488365c9a4f14bf96eab089d6ac869b675c1b4 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Thu, 31 Oct 2013 19:43:12 +0000 Subject: [PATCH 75/80] Add system.version functional test --- tests/functional/test_system.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/functional/test_system.py diff --git a/tests/functional/test_system.py b/tests/functional/test_system.py new file mode 100644 index 0000000..6b7b26c --- /dev/null +++ b/tests/functional/test_system.py @@ -0,0 +1,31 @@ +import logging +import unittest + +import trovebox +from tests.functional import test_base + +class TestSystem(test_base.TestBase): + testcase_name = "system" + + def setUp(self): + """ + Override the default setUp, since we don't need a populated database + """ + logging.info("\nRunning %s...", self.id()) + + def test_system_version(self): + """ + Check that the API version string is returned correctly + """ + client = trovebox.Trovebox(config_file=self.config_file) + version = client.system.version() + self.assertEqual(version["api"], "v%s" % trovebox.LATEST_API_VERSION) + + @unittest.skip("Diagnostics don't work with the hosted site") + def test_system_diagnostics(self): + """ + Check that the system diagnostics can be performed + """ + client = trovebox.Trovebox(config_file=self.config_file) + diagnostics = client.system.diagnostics() + self.assertIn(diagnostics, "database") From 4e616e4678b3145e0d1a1c05dda5124a1fe67f19 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 3 Nov 2013 13:12:47 +0000 Subject: [PATCH 76/80] Version bump to 0.6 --- trovebox/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trovebox/_version.py b/trovebox/_version.py index 20bed28..0a7c318 100644 --- a/trovebox/_version.py +++ b/trovebox/_version.py @@ -1,2 +1,2 @@ """Current version string""" -__version__ = "0.5.1" +__version__ = "0.6" From 5df4b293fb9edcc6a07ea8c1d2a16ac1066fc47e Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 3 Nov 2013 13:20:43 +0000 Subject: [PATCH 77/80] Updated CHANGELOG for v0.6 --- CHANGELOG | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index d700b26..6c3bee0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,10 +2,16 @@ Trovebox Python Library Changelog ================================= +v0.6 +====== + * Support for many additional API endpoints (#56, #65) + * Code coverage reporting (#57) + * Unit test improvements (#58, #63, #64) + * Ignore illegal TroveboxObject attributes (#61) + v0.5.1 ====== * Use httpretty v0.6.5 for unit tests (#60) - * Ignore illegal TroveboxObject attributes (#61) v0.5 ====== From e2d5958b50ea30d4eb1cdb0c27cffc4153fe9f4b Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 3 Nov 2013 14:07:17 +0000 Subject: [PATCH 78/80] Updated Pylint disable patchfile --- ...nt-ignores.patch => .pylint-disable.patch} | 121 +++++++----------- 1 file changed, 44 insertions(+), 77 deletions(-) rename trovebox/{.pylint-ignores.patch => .pylint-disable.patch} (62%) diff --git a/trovebox/.pylint-ignores.patch b/trovebox/.pylint-disable.patch similarity index 62% rename from trovebox/.pylint-ignores.patch rename to trovebox/.pylint-disable.patch index 27efeb7..5b96583 100644 --- a/trovebox/.pylint-ignores.patch +++ b/trovebox/.pylint-disable.patch @@ -1,28 +1,28 @@ -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_activity.py patched/api/api_activity.py ---- original/api/api_activity.py 2013-08-19 17:59:15.592149000 +0100 -+++ patched/api/api_activity.py 2013-08-19 18:08:39.950947589 +0100 -@@ -22,7 +22,7 @@ - raise TroveboxError("Purge response returned False") - return True +diff --unified --recursive '--exclude=.pylint-disable.patch' original/api/api_activity.py patched/api/api_activity.py +--- original/api/api_activity.py ++++ patched/api/api_activity.py +@@ -32,7 +32,7 @@ + """ + return self._client.post("/activities/purge.json", **kwds)["result"] --class ApiActivity(object): -+class ApiActivity(object): # pylint: disable=R0903 +-class ApiActivity(ApiBase): ++class ApiActivity(ApiBase): # pylint: disable=too-few-public-methods """ Definitions of /activity/ API endpoints """ def view(self, activity, **kwds): """ -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_album.py patched/api/api_album.py ---- original/api/api_album.py 2013-08-19 16:09:53.539609000 +0100 -+++ patched/api/api_album.py 2013-08-19 18:08:20.118849270 +0100 -@@ -3,7 +3,7 @@ - """ +diff --unified --recursive '--exclude=.pylint-disable.patch' original/api/api_album.py patched/api/api_album.py +--- original/api/api_album.py ++++ patched/api/api_album.py +@@ -7,7 +7,7 @@ from trovebox.objects.album import Album + from .api_base import ApiBase --class ApiAlbums(object): -+class ApiAlbums(object): # pylint: disable=R0903 +-class ApiAlbums(ApiBase): ++class ApiAlbums(ApiBase): # pylint: disable=too-few-public-methods """ Definitions of /albums/ API endpoints """ def list(self, **kwds): """ -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_base.py patched/api/api_base.py +diff --unified --recursive '--exclude=.pylint-disable.patch' original/api/api_base.py patched/api/api_base.py --- original/api/api_base.py +++ patched/api/api_base.py @@ -2,7 +2,7 @@ @@ -34,10 +34,11 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_ba """ Base class for all API objects """ def __init__(self, client): self._client = client -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_tag.py patched/api/api_tag.py ---- original/api/api_tag.py 2013-08-19 16:09:53.539609000 +0100 -+++ patched/api/api_tag.py 2013-08-19 18:08:20.118849270 +0100 -@@ -3,7 +3,7 @@ +diff --unified --recursive '--exclude=.pylint-disable.patch' original/api/api_tag.py patched/api/api_tag.py +--- original/api/api_tag.py ++++ patched/api/api_tag.py +@@ -2,14 +2,14 @@ + api_tag.py : Trovebox Tag API Classes """ try: - from urllib.parse import quote # Python3 @@ -45,16 +46,15 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_ta except ImportError: from urllib import quote # Python2 -@@ -10,7 +10,7 @@ from trovebox.objects.tag import Tag from .api_base import ApiBase --class ApiTags(object): -+class ApiTags(object): # pylint: disable=R0903 +-class ApiTags(ApiBase): ++class ApiTags(ApiBase): # pylint: disable=too-few-public-methods """ Definitions of /tags/ API endpoints """ def list(self, **kwds): """ -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/auth.py patched/auth.py +diff --unified --recursive '--exclude=.pylint-disable.patch' original/auth.py patched/auth.py --- original/auth.py +++ patched/auth.py @@ -4,7 +4,7 @@ @@ -87,10 +87,10 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/auth.py pa except AttributeError: parser.readfp(buf) # Python2 -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/http.py patched/http.py ---- original/http.py 2013-08-19 16:09:53.543609000 +0100 -+++ patched/http.py 2013-08-19 18:08:20.118849270 +0100 -@@ -7,18 +7,18 @@ +diff --unified --recursive '--exclude=.pylint-disable.patch' original/http.py patched/http.py +--- original/http.py ++++ patched/http.py +@@ -7,7 +7,7 @@ import requests_oauthlib import logging try: @@ -99,9 +99,7 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/http.py pa except ImportError: from urlparse import urlparse, urlunparse # Python2 - from trovebox.objects.trovebox_object import TroveboxObject --from .errors import * -+from .errors import * # pylint: disable=W0401 +@@ -16,9 +16,9 @@ from .auth import Auth if sys.version < '3': @@ -122,19 +120,10 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/http.py pa consumer_key='', consumer_secret='', token='', token_secret='', api_version=None): -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.py patched/__init__.py ---- original/__init__.py 2013-08-19 17:02:22.951226000 +0100 -+++ patched/__init__.py 2013-08-19 18:08:36.194928993 +0100 -@@ -2,7 +2,7 @@ - __init__.py : Trovebox package top level - """ - from .http import Http --from .errors import * -+from .errors import * # pylint: disable=W0401 - from ._version import __version__ - from trovebox.api import api_photo - from trovebox.api import api_tag -@@ -12,7 +12,7 @@ +diff --unified --recursive '--exclude=.pylint-disable.patch' original/__init__.py patched/__init__.py +--- original/__init__.py ++++ patched/__init__.py +@@ -13,7 +13,7 @@ LATEST_API_VERSION = 2 @@ -143,7 +132,7 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.p """ Client library for Trovebox If no parameters are specified, config is loaded from the default -@@ -24,7 +24,7 @@ +@@ -25,7 +25,7 @@ This should be used to ensure that your application will continue to work even if the Trovebox API is updated to a new revision. """ @@ -152,7 +141,7 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.p consumer_key='', consumer_secret='', token='', token_secret='', api_version=None): -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/main.py patched/main.py +diff --unified --recursive '--exclude=.pylint-disable.patch' original/main.py patched/main.py --- original/main.py +++ patched/main.py @@ -26,7 +26,7 @@ @@ -160,11 +149,11 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/main.py pa ################################################################# -def main(args=sys.argv[1:]): -+def main(args=sys.argv[1:]): # pylint: disable=R0912,C0111 ++def main(args=sys.argv[1:]): # pylint: disable=too-many-branches + """Run the commandline script""" usage = "%prog --help" parser = OptionParser(usage, add_help_option=False) - parser.add_option('-c', '--config', help="Configuration file to use", -@@ -84,13 +84,13 @@ +@@ -85,11 +85,11 @@ sys.exit(1) if options.method == "GET": @@ -176,34 +165,12 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/main.py pa - result = client.post(options.endpoint, process_response=False, + result = client.post(options.endpoint, process_response=False, # pylint: disable=star-args files=files, **params) -- for f in files: -+ for f in files: # pylint: disable=C0103 - files[f].close() - - if options.verbose: -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/tag.py patched/objects/tag.py ---- original/objects/tag.py 2013-08-19 16:09:53.543609000 +0100 -+++ patched/objects/tag.py 2013-08-19 18:08:20.118849270 +0100 -@@ -1,8 +1,8 @@ --""" -+""" # pylint: disable=R0801 - Representation of a Tag object - """ - try: -- from urllib.parse import quote # Python3 -+ from urllib.parse import quote # Python3 # pylint: disable=F0401,E0611 - except ImportError: - from urllib import quote # Python2 - -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/trovebox_object.py patched/objects/trovebox_object.py ---- original/objects/trovebox_object.py 2013-08-19 16:09:53.543609000 +0100 -+++ patched/objects/trovebox_object.py 2013-08-19 18:08:20.118849270 +0100 -@@ -1,10 +1,10 @@ - """ - Base object supporting the storage of custom fields as attributes - """ --class TroveboxObject(object): -+class TroveboxObject(object): # pylint: disable=R0903 + for file_ in files: + files[file_].close() +diff --unified --recursive '--exclude=.pylint-disable.patch' original/objects/trovebox_object.py patched/objects/trovebox_object.py +--- original/objects/trovebox_object.py ++++ patched/objects/trovebox_object.py +@@ -5,7 +5,7 @@ """ Base object supporting the storage of custom fields as attributes """ _type = "None" def __init__(self, client, json_dict): From f6ab83a47014760da6f3e86942579dad04ba0997 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 3 Nov 2013 14:12:05 +0000 Subject: [PATCH 79/80] Fix CHANGELOG --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 6c3bee0..bcc9000 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,11 +7,11 @@ v0.6 * Support for many additional API endpoints (#56, #65) * Code coverage reporting (#57) * Unit test improvements (#58, #63, #64) - * Ignore illegal TroveboxObject attributes (#61) v0.5.1 ====== * Use httpretty v0.6.5 for unit tests (#60) + * Ignore illegal TroveboxObject attributes (#61) v0.5 ====== From 9769fa04902b89e4aa02ca2043a876a535d08f75 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 3 Nov 2013 14:21:04 +0000 Subject: [PATCH 80/80] Delete accidentally committed files --- trovebox/.pylint-ignores.patch.orig | 182 ---------------------------- trovebox/_version.py.orig | 7 -- 2 files changed, 189 deletions(-) delete mode 100644 trovebox/.pylint-ignores.patch.orig delete mode 100644 trovebox/_version.py.orig diff --git a/trovebox/.pylint-ignores.patch.orig b/trovebox/.pylint-ignores.patch.orig deleted file mode 100644 index af3e9da..0000000 --- a/trovebox/.pylint-ignores.patch.orig +++ /dev/null @@ -1,182 +0,0 @@ -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_activity.py patched/api/api_activity.py ---- original/api/api_activity.py -+++ patched/api/api_activity.py -@@ -33,7 +33,7 @@ - raise TroveboxError("Purge response returned False") - return True - --class ApiActivity(ApiBase): -+class ApiActivity(ApiBase): # pylint: disable=too-few-public-methods - """ Definitions of /activity/ API endpoints """ - def view(self, activity, **kwds): - """ -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_album.py patched/api/api_album.py ---- original/api/api_album.py -+++ patched/api/api_album.py -@@ -8,7 +8,7 @@ - from trovebox.objects.album import Album - from .api_base import ApiBase - --class ApiAlbums(ApiBase): -+class ApiAlbums(ApiBase): # pylint: disable=too-few-public-methods - """ Definitions of /albums/ API endpoints """ - def list(self, **kwds): - """ -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_base.py patched/api/api_base.py ---- original/api/api_base.py -+++ patched/api/api_base.py -@@ -2,7 +2,7 @@ - api_base.py: Base class for all API classes - """ - --class ApiBase(object): -+class ApiBase(object): # pylint: disable=too-few-public-methods - """ Base class for all API objects """ - def __init__(self, client): - self._client = client -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_tag.py patched/api/api_tag.py ---- original/api/api_tag.py -+++ patched/api/api_tag.py -@@ -2,7 +2,7 @@ - api_tag.py : Trovebox Tag API Classes - """ - try: -- from urllib.parse import quote # Python3 -+ from urllib.parse import quote # Python3 # pylint: disable=import-error,no-name-in-module - except ImportError: - from urllib import quote # Python2 - -@@ -10,7 +10,7 @@ - from trovebox.objects.tag import Tag - from .api_base import ApiBase - --class ApiTags(ApiBase): -+class ApiTags(ApiBase): # pylint: disable=too-few-public-methods - """ Definitions of /tags/ API endpoints """ - def list(self, **kwds): - """ -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/auth.py patched/auth.py ---- original/auth.py -+++ patched/auth.py -@@ -4,7 +4,7 @@ - from __future__ import unicode_literals - import os - try: -- from configparser import ConfigParser # Python3 -+ from configparser import ConfigParser # Python3 # pylint: disable=import-error - except ImportError: - from ConfigParser import SafeConfigParser as ConfigParser # Python2 - try: -@@ -12,9 +12,9 @@ - except ImportError: # pragma: no cover - import StringIO as io # Python2 - --class Auth(object): -+class Auth(object): # pylint: disable=too-few-public-methods - """OAuth secrets""" -- def __init__(self, config_file, host, -+ def __init__(self, config_file, host, # pylint: disable=too-many-arguments - consumer_key, consumer_secret, - token, token_secret): - if host is None: -@@ -69,7 +69,7 @@ - parser = ConfigParser() - parser.optionxform = str # Case-sensitive options - try: -- parser.read_file(buf) # Python3 -+ parser.read_file(buf) # Python3 # pylint: disable=maybe-no-member - except AttributeError: - parser.readfp(buf) # Python2 - -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/http.py patched/http.py ---- original/http.py -+++ patched/http.py -@@ -7,7 +7,7 @@ - import requests_oauthlib - import logging - try: -- from urllib.parse import urlparse, urlunparse # Python3 -+ from urllib.parse import urlparse, urlunparse # Python3 # pylint: disable=import-error,no-name-in-module - except ImportError: - from urlparse import urlparse, urlunparse # Python2 - -@@ -16,9 +16,9 @@ - from .auth import Auth - - if sys.version < '3': -- TEXT_TYPE = unicode -+ TEXT_TYPE = unicode # pylint: disable=invalid-name - else: # pragma: no cover -- TEXT_TYPE = str -+ TEXT_TYPE = str # pylint: disable=invalid-name - - DUPLICATE_RESPONSE = {"code": 409, - "message": "This photo already exists"} -@@ -37,7 +37,7 @@ - "ssl_verify" : True, - } - -- def __init__(self, config_file=None, host=None, -+ def __init__(self, config_file=None, host=None, # pylint: disable=too-many-arguments - consumer_key='', consumer_secret='', - token='', token_secret='', api_version=None): - -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.py patched/__init__.py ---- original/__init__.py -+++ patched/__init__.py -@@ -12,7 +12,7 @@ - - LATEST_API_VERSION = 2 - --class Trovebox(Http): -+class Trovebox(Http): # pylint: disable=too-many-instance-attributes - """ - Client library for Trovebox - If no parameters are specified, config is loaded from the default -@@ -24,7 +24,7 @@ - This should be used to ensure that your application will continue to work - even if the Trovebox API is updated to a new revision. - """ -- def __init__(self, config_file=None, host=None, -+ def __init__(self, config_file=None, host=None, # pylint: disable=too-many-arguments - consumer_key='', consumer_secret='', - token='', token_secret='', - api_version=None): -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/main.py patched/main.py ---- original/main.py -+++ patched/main.py -@@ -26,7 +26,7 @@ - - ################################################################# - --def main(args=sys.argv[1:]): -+def main(args=sys.argv[1:]): # pylint: disable=too-many-branches - """Run the commandline script""" - usage = "%prog --help" - parser = OptionParser(usage, add_help_option=False) -@@ -85,11 +85,11 @@ - sys.exit(1) - - if options.method == "GET": -- result = client.get(options.endpoint, process_response=False, -+ result = client.get(options.endpoint, process_response=False, # pylint: disable=star-args - **params) - else: - params, files = extract_files(params) -- result = client.post(options.endpoint, process_response=False, -+ result = client.post(options.endpoint, process_response=False, # pylint: disable=star-args - files=files, **params) - for file_ in files: - files[file_].close() -diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/trovebox_object.py patched/objects/trovebox_object.py ---- original/objects/trovebox_object.py -+++ patched/objects/trovebox_object.py -@@ -5,7 +5,7 @@ - """ Base object supporting the storage of custom fields as attributes """ - _type = "None" - def __init__(self, client, json_dict): -- self.id = None -+ self.id = None # pylint: disable=invalid-name - self.name = None - self._client = client - self._json_dict = json_dict diff --git a/trovebox/_version.py.orig b/trovebox/_version.py.orig deleted file mode 100644 index 444ca0a..0000000 --- a/trovebox/_version.py.orig +++ /dev/null @@ -1,7 +0,0 @@ -<<<<<<< HEAD - -__version__ = "0.5.1" -======= -"""Current version string""" -__version__ = "0.5" ->>>>>>> 8f488365c9a4f14bf96eab089d6ac869b675c1b4