From 4147029b14b65a07a6fc44241b97d30140018777 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Mon, 10 Sep 2012 23:57:42 +0100 Subject: [PATCH 01/25] Add photo.transform support and testcase. --- openphoto/api_photo.py | 11 ++++++++++- openphoto/objects.py | 11 +++++++++-- tests/test_photos.py | 22 +++++++++++++++++++--- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/openphoto/api_photo.py b/openphoto/api_photo.py index 0686a88..63f815f 100644 --- a/openphoto/api_photo.py +++ b/openphoto/api_photo.py @@ -89,4 +89,13 @@ class ApiPhoto: return photo.next_previous(**kwds) def transform(self, photo, **kwds): - raise NotImplementedError() + """ + Performs transformation specified in **kwds + Example: transform(photo, rotate=90) + """ + if not isinstance(photo, Photo): + photo = Photo(self._client, {"id": photo}) + photo.transform(**kwds) + # The API doesn't currently return the transformed photo + # Uncomment the below once frontend issue #955 is resolved +# return photo diff --git a/openphoto/objects.py b/openphoto/objects.py index a2328bf..82b8250 100644 --- a/openphoto/objects.py +++ b/openphoto/objects.py @@ -85,8 +85,15 @@ class Photo(OpenPhotoObject): return value def transform(self, **kwds): - raise NotImplementedError() - + """ + Performs transformation specified in **kwds + Example: transform(rotate=90) + """ + new_dict = self._openphoto.post("/photo/%s/transform.json" % self.id, + **kwds)["result"] + # The API doesn't currently return the transformed photo + # Uncomment the below once frontend issue #955 is resolved +# self._replace_fields(new_dict) class Tag(OpenPhotoObject): def delete(self, **kwds): diff --git a/tests/test_photos.py b/tests/test_photos.py index 68e9cac..e12d47a 100644 --- a/tests/test_photos.py +++ b/tests/test_photos.py @@ -152,6 +152,22 @@ class TestPhotos(test_base.TestBase): self.client.photo.dynamic_url(None) def test_transform(self): - """ If photo.transform gets implemented, write a test! """ - with self.assertRaises(openphoto.NotImplementedError): - self.client.photo.transform(None) + """ Test photo rotation """ + photo = self.photos[0] + self.assertEqual(photo.rotation, "0") + photo = self.client.photo.transform(photo, rotate=90) + + # Need an explicit update, since transform API doesn't return the rotated photo + # Remove the following line once Issue #955 is resolved + photo = self.client.photo.view(self.photos[0]) + + self.assertEqual(photo.rotation, "90") + + # Do the same using the Photo object directly + photo.transform(rotate=90) + + # Need an explicit update, since transform API doesn't return the rotated photo + # Remove the following line once Issue #955 is resolved + photo = self.client.photo.view(photo) + + self.assertEqual(photo.rotation, "180") From 07745f149659918e40600e636a5c6049fb240346 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 23 Mar 2013 16:45:24 +0000 Subject: [PATCH 02/25] Frontend issue 937 is now resolved - album.update now returns the updated album --- openphoto/api_album.py | 7 ++----- openphoto/objects.py | 10 ++-------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/openphoto/api_album.py b/openphoto/api_album.py index e997b65..cda3687 100644 --- a/openphoto/api_album.py +++ b/openphoto/api_album.py @@ -1,4 +1,4 @@ -from errors import * +< Date: Sat, 23 Mar 2013 16:46:47 +0000 Subject: [PATCH 03/25] Prettier printout when running tests --- tests/test_base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index 9f89315..a905bbf 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -62,7 +62,7 @@ class TestBase(unittest.TestCase): self.photos = self.client.photos.list() if len(self.photos) != 3: # print self.photos - print "[Regenerating Photos]" + print "\n[Regenerating Photos]" if len(self.photos) > 0: self._delete_all() self._create_test_photos() @@ -72,7 +72,7 @@ class TestBase(unittest.TestCase): if (len(self.tags) != 1 or self.tags[0].id != self.TEST_TAG or self.tags[0].count != 3): - print "[Regenerating Tags]" + print "\n[Regenerating Tags]" self._delete_all() self._create_test_photos() self.photos = self.client.photos.list() @@ -85,7 +85,7 @@ class TestBase(unittest.TestCase): if (len(self.albums) != 1 or self.albums[0].name != self.TEST_ALBUM or self.albums[0].count != "3"): - print "[Regenerating Albums]" + print "\n[Regenerating Albums]" self._delete_all() self._create_test_photos() self.photos = self.client.photos.list() From 1ce5192d70406cb4b9fc8db54db485f6144e964b Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 23 Mar 2013 16:35:16 +0000 Subject: [PATCH 04/25] Fixed typo --- openphoto/api_album.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openphoto/api_album.py b/openphoto/api_album.py index cda3687..55e734f 100644 --- a/openphoto/api_album.py +++ b/openphoto/api_album.py @@ -1,4 +1,4 @@ -< Date: Sat, 23 Mar 2013 18:17:09 +0000 Subject: [PATCH 05/25] Frontend issue 927 is now fixed, so the tag.create endpoint can be used. The tag.create endpoint doesn't return the newly created tag - it is invisible, since it has no photos. --- openphoto/api_tag.py | 5 ++--- tests/test_tags.py | 30 ++++++++++++++++-------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/openphoto/api_tag.py b/openphoto/api_tag.py index 89f9fee..464cc9d 100644 --- a/openphoto/api_tag.py +++ b/openphoto/api_tag.py @@ -15,9 +15,8 @@ class ApiTag: self._client = client def create(self, tag, **kwds): - """ Create a new tag and return it """ - result = self._client.post("/tag/create.json", tag=tag, **kwds)["result"] - return Tag(self._client, result) + """ Create a new tag. The API returns true if the tag was sucessfully created """ + return self._client.post("/tag/create.json", tag=tag, **kwds)["result"] def delete(self, tag, **kwds): """ Delete a tag """ diff --git a/tests/test_tags.py b/tests/test_tags.py index 02ea449..6320c2f 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -3,31 +3,33 @@ import openphoto import test_base class TestTags(test_base.TestBase): - @unittest.expectedFailure # Tag create fails - Issue #927 - # NOTE: the below has not been tested/debugged, since it fails at the first step - def test_create_delete(self, tag_name="create_tag"): + def test_create_delete(self, tag_id="create_tag"): """ Create a tag then delete it """ # Create a tag - tag = self.client.tag.create(tag_name) + self.assertTrue(self.client.tag.create(tag_id)) + # Check that the tag doesn't exist (It has no photos, so it's invisible) + self.assertNotIn(tag_id, [t.id for t in self.client.tags.list()]) - # Check the return value - self.assertEqual(tag.id, tag_name) + # Create a tag on one of the photos + self.photos[0].update(tagsAdd=tag_id) # Check that the tag now exists - self.assertIn(tag_name, self.client.tags.list()) + self.assertIn(tag_id, [t.id for t in self.client.tags.list()]) # Delete the tag - self.client.tag.delete(tag_name) + self.client.tag.delete(tag_id) # Check that the tag is now gone - self.assertNotIn(tag_name, self.client.tags.list()) + self.assertNotIn(tag_id, [t.id for t in self.client.tags.list()]) - # Create and delete using the Tag object directly - tag = self.client.tag.create(tag_name) + # Create then delete using the Tag object directly + self.photos[0].update(tagsAdd=tag_id) + tag = [t for t in self.client.tags.list() if t.id == tag_id][0] tag.delete() # Check that the tag is now gone - self.assertNotIn(tag_name, self.client.tags.list()) + self.assertNotIn(tag_id, [t.id for t in self.client.tags.list()]) - @unittest.expectedFailure # Tag update fails - Issue #927 - # NOTE: the below has not been tested/debugged, since it fails at the first step + # NOTE: this test doesn't work, since it's not possible to update the tag owner + # It's unclear what tag/update is for, since there are no fields that can be updated! + @unittest.skip def test_update(self): """ Test that a tag can be updated """ # Update the tag using the OpenPhoto class, passing in the tag object From ac5d5895a138f69fd93fb43d7d67d860cfc175ac Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 23 Mar 2013 18:25:54 +0000 Subject: [PATCH 06/25] Ensure tag endpoints are correctly escaped --- openphoto/objects.py | 5 +++-- tests/test_tags.py | 2 -- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openphoto/objects.py b/openphoto/objects.py index 965df9a..062ed92 100644 --- a/openphoto/objects.py +++ b/openphoto/objects.py @@ -1,3 +1,4 @@ +import urllib from errors import * class OpenPhotoObject: @@ -98,12 +99,12 @@ class Photo(OpenPhotoObject): class Tag(OpenPhotoObject): def delete(self, **kwds): """ Delete this tag """ - self._openphoto.post("/tag/%s/delete.json" % self.id, **kwds) + self._openphoto.post("/tag/%s/delete.json" % urllib.quote(self.id), **kwds) self._replace_fields({}) def update(self, **kwds): """ Update this tag with the specified parameters """ - new_dict = self._openphoto.post("/tag/%s/update.json" % self.id, + new_dict = self._openphoto.post("/tag/%s/update.json" % urllib.quote(self.id), **kwds)["result"] self._replace_fields(new_dict) diff --git a/tests/test_tags.py b/tests/test_tags.py index 6320c2f..0e85d30 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -59,8 +59,6 @@ class TestTags(test_base.TestBase): self.assertEqual(self.tags[0].owner, owner) self.assertEqual(ret_val.owner, owner) - @unittest.expectedFailure # Tag create fails - Issue #927 - # NOTE: the below has not been tested/debugged, since it fails at the first step def test_tag_with_spaces(self): """ Run test_create_delete using a tag containing spaces """ self.test_create_delete("tag with spaces") From 3c6b44846072154a349c9acfd6b5536bb0842360 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 23 Mar 2013 18:56:39 +0000 Subject: [PATCH 07/25] Tag test updates. Can't find a way of testing tag.update, since there are no updatable tag fields. Tags with slashes work Tags with double slashes don't, but at least they're no longer undeletable. --- tests/test_tags.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/test_tags.py b/tests/test_tags.py index 0e85d30..a90e744 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -27,9 +27,9 @@ class TestTags(test_base.TestBase): # Check that the tag is now gone self.assertNotIn(tag_id, [t.id for t in self.client.tags.list()]) - # NOTE: this test doesn't work, since it's not possible to update the tag owner - # It's unclear what tag/update is for, since there are no fields that can be updated! - @unittest.skip + # TODO: Un-skip and update this tests once there are tag fields that can be updated. + # The owner field cannot be updated. + @unittest.skip("Can't test the tag.update endpoint, since there are no fields that can be updated") def test_update(self): """ Test that a tag can be updated """ # Update the tag using the OpenPhoto class, passing in the tag object @@ -63,9 +63,13 @@ class TestTags(test_base.TestBase): """ Run test_create_delete using a tag containing spaces """ self.test_create_delete("tag with spaces") - # We mustn't run this test until Issue #919 is resolved, - # since it creates an undeletable tag - @unittest.skip("Tags with double-slashes cannot be deleted - Issue #919") - def test_tag_with_double_slashes(self): + def test_tag_with_slashes(self): """ Run test_create_delete using a tag containing slashes """ - self.test_create_delete("tag/with//slashes") + self.test_create_delete("tag/with/slashes") + + # TODO: Un-skip this test once issue #919 is resolved - + # tags with double-slashes cannot be deleted + @unittest.expectedFailure + 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 9333ad100ede13e1510ed6cf82c653a9641fda7e Mon Sep 17 00:00:00 2001 From: Pete Date: Tue, 9 Apr 2013 17:43:00 +0100 Subject: [PATCH 08/25] Fixed helpful test error message --- tests/test_base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index ad425b1..c184746 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -7,12 +7,11 @@ try: except ImportError: print ("********************************************************************\n" "You need to create a 'tokens.py' file containing the following:\n\n" - " host = \"\"\n" + " host = \"\"\n" " consumer_key = \"\"\n" " consumer_secret = \"\"\n" " token = \"\"\n" " token_secret = \"\"\n" - " host = \"\"\n\n" "WARNING: Don't use a production OpenPhoto instance for this!\n" "********************************************************************\n") raise From e09f1667435370ef7f158768d0e331bb276510c2 Mon Sep 17 00:00:00 2001 From: Pete Date: Tue, 9 Apr 2013 18:18:12 +0100 Subject: [PATCH 09/25] Update delete endpoints to return True if successful. If the operation isn't successful, the API returns an error code, which raises an OpenPhotoError exception. --- openphoto/api_album.py | 10 +++++++--- openphoto/api_photo.py | 23 ++++++++++++++++++----- openphoto/api_tag.py | 8 ++++++-- openphoto/objects.py | 27 +++++++++++++++++++++------ tests/test_albums.py | 4 ++-- tests/test_photos.py | 8 ++++---- tests/test_tags.py | 4 ++-- 7 files changed, 60 insertions(+), 24 deletions(-) diff --git a/openphoto/api_album.py b/openphoto/api_album.py index e997b65..f2f2644 100644 --- a/openphoto/api_album.py +++ b/openphoto/api_album.py @@ -20,11 +20,15 @@ class ApiAlbum: return Album(self._client, result) def delete(self, album, **kwds): - """ Delete an album """ + """ + Delete an album. + Returns True if successful. + Raises an OpenPhotoError if not. + """ if not isinstance(album, Album): album = Album(self._client, {"id": album}) - album.delete(**kwds) - + return album.delete(**kwds) + def form(self, album, **kwds): raise NotImplementedError() diff --git a/openphoto/api_photo.py b/openphoto/api_photo.py index fed9c8a..2c18e8e 100644 --- a/openphoto/api_photo.py +++ b/openphoto/api_photo.py @@ -14,25 +14,38 @@ class ApiPhotos: return [Photo(self._client, photo) for photo in photos] def update(self, photos, **kwds): - """ Updates a list of photos """ + """ + Updates a list of photos. + Returns True if successful. + Raises OpenPhotoError if not. + """ if not self._client.post("/photos/update.json", ids=photos, **kwds)["result"]: raise OpenPhotoError("Update response returned False") + return True def delete(self, photos, **kwds): - """ Deletes a list of photos """ + """ + Deletes a list of photos. + Returns True if successful. + Raises OpenPhotoError if not. + """ if not self._client.post("/photos/delete.json", ids=photos, **kwds)["result"]: raise OpenPhotoError("Delete response returned False") - + return True class ApiPhoto: def __init__(self, client): self._client = client def delete(self, photo, **kwds): - """ Delete a photo """ + """ + Delete a photo. + Returns True if successful. + Raises an OpenPhotoError if not. + """ if not isinstance(photo, Photo): photo = Photo(self._client, {"id": photo}) - photo.delete(**kwds) + return photo.delete(**kwds) def edit(self, photo, **kwds): """ Returns an HTML form to edit a photo """ diff --git a/openphoto/api_tag.py b/openphoto/api_tag.py index 89f9fee..5f3c676 100644 --- a/openphoto/api_tag.py +++ b/openphoto/api_tag.py @@ -20,10 +20,14 @@ class ApiTag: return Tag(self._client, result) def delete(self, tag, **kwds): - """ Delete a tag """ + """ + Delete a tag. + Returns True if successful. + Raises an OpenPhotoError if not. + """ if not isinstance(tag, Tag): tag = Tag(self._client, {"id": tag}) - tag.delete(**kwds) + return tag.delete(**kwds) def update(self, tag, **kwds): """ Update a tag """ diff --git a/openphoto/objects.py b/openphoto/objects.py index 965df9a..d85a780 100644 --- a/openphoto/objects.py +++ b/openphoto/objects.py @@ -39,9 +39,14 @@ class OpenPhotoObject: class Photo(OpenPhotoObject): def delete(self, **kwds): - """ Delete this photo """ - self._openphoto.post("/photo/%s/delete.json" % self.id, **kwds) + """ + Delete this photo. + Returns True if successful. + Raises an OpenPhotoError if not. + """ + result = self._openphoto.post("/photo/%s/delete.json" % self.id, **kwds)["result"] self._replace_fields({}) + return result def edit(self, **kwds): """ Returns an HTML form to edit the photo """ @@ -97,9 +102,14 @@ class Photo(OpenPhotoObject): class Tag(OpenPhotoObject): def delete(self, **kwds): - """ Delete this tag """ - self._openphoto.post("/tag/%s/delete.json" % self.id, **kwds) + """ + Delete this tag. + Returns True if successful. + Raises an OpenPhotoError if not. + """ + result = self._openphoto.post("/tag/%s/delete.json" % self.id, **kwds)["result"] self._replace_fields({}) + return result def update(self, **kwds): """ Update this tag with the specified parameters """ @@ -125,9 +135,14 @@ class Album(OpenPhotoObject): self.photos[i] = Photo(self._openphoto, photo) def delete(self, **kwds): - """ Delete this album """ - self._openphoto.post("/album/%s/delete.json" % self.id, **kwds) + """ + Delete this album. + Returns True if successful. + Raises an OpenPhotoError if not. + """ + result = self._openphoto.post("/album/%s/delete.json" % self.id, **kwds)["result"] self._replace_fields({}) + return result def form(self, **kwds): raise NotImplementedError() diff --git a/tests/test_albums.py b/tests/test_albums.py index 53ff7e9..563660f 100644 --- a/tests/test_albums.py +++ b/tests/test_albums.py @@ -15,13 +15,13 @@ class TestAlbums(test_base.TestBase): self.assertIn(album_name, [a.name for a in self.client.albums.list()]) # Delete the album - self.client.album.delete(album.id) + self.assertTrue(self.client.album.delete(album.id)) # Check that the album is now gone self.assertNotIn(album_name, [a.name for a in self.client.albums.list()]) # Create it again, and delete it using the Album object album = self.client.album.create(album_name) - album.delete() + self.assertTrue(album.delete()) # Check that the album is now gone self.assertNotIn(album_name, [a.name for a in self.client.albums.list()]) diff --git a/tests/test_photos.py b/tests/test_photos.py index ffddbfe..04e216d 100644 --- a/tests/test_photos.py +++ b/tests/test_photos.py @@ -6,11 +6,11 @@ class TestPhotos(test_base.TestBase): def test_delete_upload(self): """ Test photo deletion and upload """ # Delete one photo using the OpenPhoto class, passing in the id - self.client.photo.delete(self.photos[0].id) + self.assertTrue(self.client.photo.delete(self.photos[0].id)) # Delete one photo using the OpenPhoto class, passing in the object - self.client.photo.delete(self.photos[1]) + self.assertTrue(self.client.photo.delete(self.photos[1])) # And another using the Photo object directly - self.photos[2].delete() + self.assertTrue(self.photos[2].delete()) # Check that they're gone self.assertEqual(self.client.photos.list(), []) @@ -32,7 +32,7 @@ class TestPhotos(test_base.TestBase): self.assertIn(ret_val.pathOriginal, pathOriginals) # Delete all photos in one go - self.client.photos.delete(self.photos) + self.assertTrue(self.client.photos.delete(self.photos)) # Check they're gone self.photos = self.client.photos.list() diff --git a/tests/test_tags.py b/tests/test_tags.py index 02ea449..7f06ed2 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -16,13 +16,13 @@ class TestTags(test_base.TestBase): self.assertIn(tag_name, self.client.tags.list()) # Delete the tag - self.client.tag.delete(tag_name) + self.assertTrue(self.client.tag.delete(tag_name)) # Check that the tag is now gone self.assertNotIn(tag_name, self.client.tags.list()) # Create and delete using the Tag object directly tag = self.client.tag.create(tag_name) - tag.delete() + self.assertTrue(tag.delete()) # Check that the tag is now gone self.assertNotIn(tag_name, self.client.tags.list()) From ea904e4337e5d74976bddcc3f36330411b4aea38 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 13 Apr 2013 13:11:26 +0100 Subject: [PATCH 10/25] Not that frontend issue 955 is resolved, we should use the return value of the transform API to replace the photo object's fields --- openphoto/api_photo.py | 4 +--- openphoto/objects.py | 6 ++---- tests/test_photos.py | 10 ---------- 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/openphoto/api_photo.py b/openphoto/api_photo.py index 6e26ea8..18ae68c 100644 --- a/openphoto/api_photo.py +++ b/openphoto/api_photo.py @@ -111,6 +111,4 @@ class ApiPhoto: if not isinstance(photo, Photo): photo = Photo(self._client, {"id": photo}) photo.transform(**kwds) - # The API doesn't currently return the transformed photo - # Uncomment the below once frontend issue #955 is resolved -# return photo + return photo diff --git a/openphoto/objects.py b/openphoto/objects.py index 17484f5..1d09f43 100644 --- a/openphoto/objects.py +++ b/openphoto/objects.py @@ -98,14 +98,12 @@ class Photo(OpenPhotoObject): def transform(self, **kwds): """ - Performs transformation specified in **kwds + Performs transformation specified in **kwds Example: transform(rotate=90) """ new_dict = self._openphoto.post("/photo/%s/transform.json" % self.id, **kwds)["result"] - # The API doesn't currently return the transformed photo - # Uncomment the below once frontend issue #955 is resolved -# self._replace_fields(new_dict) + self._replace_fields(new_dict) class Tag(OpenPhotoObject): def delete(self, **kwds): diff --git a/tests/test_photos.py b/tests/test_photos.py index 8c48e53..0050298 100644 --- a/tests/test_photos.py +++ b/tests/test_photos.py @@ -151,18 +151,8 @@ class TestPhotos(test_base.TestBase): photo = self.photos[0] self.assertEqual(photo.rotation, "0") photo = self.client.photo.transform(photo, rotate=90) - - # Need an explicit update, since transform API doesn't return the rotated photo - # Remove the following line once Issue #955 is resolved - photo = self.client.photo.view(self.photos[0]) - self.assertEqual(photo.rotation, "90") # Do the same using the Photo object directly photo.transform(rotate=90) - - # Need an explicit update, since transform API doesn't return the rotated photo - # Remove the following line once Issue #955 is resolved - photo = self.client.photo.view(photo) - self.assertEqual(photo.rotation, "180") From e6095ec3db11c3db04e6bb00167527a1e0372a61 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Mon, 15 Apr 2013 21:30:06 +0100 Subject: [PATCH 11/25] Update README to stop nosetests from capturing the logging, since it gets pretty large. Logs are saved to tests.log. --- tests/README.markdown | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/README.markdown b/tests/README.markdown index 92e250f..89838c9 100644 --- a/tests/README.markdown +++ b/tests/README.markdown @@ -9,7 +9,7 @@ A computer, Python 2.7 and an empty OpenPhoto instance. --------------------------------------- -### Setting up +### Setting up Create a tests/tokens.py file containing the following: @@ -34,7 +34,9 @@ The "-c" lets you stop the tests gracefully with \[CTRL\]-c. The easiest way to run a subset of the tests is with nose: cd /path/to/openphoto-python - nosetests -v -s tests/test_albums.py:TestAlbums.test_view + nosetests -v -s --nologcapture tests/test_albums.py:TestAlbums.test_view + +All HTTP requests and responses are recorded in the file "tests.log". --------------------------------------- From afde3b2231f2dde4ca39b83c8ffef9a1d1d1e0c8 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Mon, 15 Apr 2013 21:32:54 +0100 Subject: [PATCH 12/25] Add API versioning support, including tests --- openphoto/__init__.py | 22 +++++++++++++----- openphoto/openphoto_http.py | 23 +++++++++++++++---- tests/test_framework.py | 46 +++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 tests/test_framework.py diff --git a/openphoto/__init__.py b/openphoto/__init__.py index 7e497bd..8132570 100644 --- a/openphoto/__init__.py +++ b/openphoto/__init__.py @@ -4,15 +4,25 @@ import api_photo import api_tag import api_album +LATEST_API_VERSION = 2 + class OpenPhoto(OpenPhotoHttp): - """ Client library for OpenPhoto """ - def __init__(self, host, + """ + Python client library for the specified OpenPhoto host. + OAuth tokens (consumer*, token*) can optionally be specified. + + All requests will include the api_version path, if specified. + This should be used to ensure that your application will continue to work + even if the OpenPhoto API is updated to a new revision. + """ + def __init__(self, host, consumer_key='', consumer_secret='', - token='', token_secret=''): - OpenPhotoHttp.__init__(self, host, + token='', token_secret='', + api_version=None): + OpenPhotoHttp.__init__(self, host, consumer_key, consumer_secret, - token, token_secret) - + token, token_secret, api_version) + self.photos = api_photo.ApiPhotos(self) self.photo = api_photo.ApiPhoto(self) self.tags = api_tag.ApiTags(self) diff --git a/openphoto/openphoto_http.py b/openphoto/openphoto_http.py index 59431f6..3422aba 100644 --- a/openphoto/openphoto_http.py +++ b/openphoto/openphoto_http.py @@ -19,12 +19,13 @@ DUPLICATE_RESPONSE = {"code": 409, class OpenPhotoHttp: """ Base class to handle HTTP requests to an OpenPhoto server """ def __init__(self, host, consumer_key='', consumer_secret='', - token='', token_secret=''): + token='', token_secret='', api_version=None): self._host = host self._consumer_key = consumer_key self._consumer_secret = consumer_secret self._token = token self._token_secret = token_secret + self._api_version = api_version self._logger = logging.getLogger("openphoto") @@ -37,11 +38,18 @@ class OpenPhotoHttp: """ Performs an HTTP GET from the specified endpoint (API path), passing parameters if given. - Returns the decoded JSON dictionary, and raises exceptions if an + The api_version is prepended to the endpoint, + if it was specified when the OpenPhoto object was created. + + Returns the decoded JSON dictionary, and raises exceptions if an error code is received. Returns the raw response if process_response=False """ params = self._process_params(params) + if not endpoint.startswith("/"): + endpoint = "/" + endpoint + if self._api_version is not None: + endpoint = "/v%d%s" % (self._api_version, endpoint) url = urlparse.urlunparse(('http', self._host, endpoint, '', urllib.urlencode(params), '')) if self._consumer_key: @@ -72,13 +80,20 @@ class OpenPhotoHttp: """ Performs an HTTP POST to the specified endpoint (API path), passing parameters if given. - Returns the decoded JSON dictionary, and raises exceptions if an + The api_version is prepended to the endpoint, + if it was specified when the OpenPhoto object was created. + + Returns the decoded JSON dictionary, and raises exceptions if an error code is received. Returns the raw response if process_response=False """ params = self._process_params(params) + if not endpoint.startswith("/"): + endpoint = "/" + endpoint + if self._api_version is not None: + endpoint = "/v%d%s" % (self._api_version, endpoint) url = urlparse.urlunparse(('http', self._host, endpoint, '', '', '')) - + if not self._consumer_key: raise OpenPhotoError("Cannot issue POST without OAuth tokens") diff --git a/tests/test_framework.py b/tests/test_framework.py new file mode 100644 index 0000000..2627e9e --- /dev/null +++ b/tests/test_framework.py @@ -0,0 +1,46 @@ +import unittest +import logging +import openphoto +import test_base + +class TestFramework(test_base.TestBase): + def setUp(self): + """Override the default setUp, since we don't need a populated database""" + logging.info("\nRunning %s..." % self.id()) + + def create_client_from_base(self, api_version): + return openphoto.OpenPhoto(self.client._host, + self.client._consumer_key, + self.client._consumer_secret, + self.client._token, + self.client._token_secret, + api_version=api_version) + + def test_api_version_zero(self): + # API v0 has a special hello world message + client = self.create_client_from_base(api_version=0) + result = client.get("hello.json") + self.assertEqual(result['message'], "Hello, world! This is version zero of the API!") + self.assertEqual(result['result']['__route__'], "/v0/hello.json") + + def test_specified_api_version(self): + # For all API versions >0, we get a generic hello world message + for api_version in range(1, openphoto.LATEST_API_VERSION + 1): + client = self.create_client_from_base(api_version=api_version) + result = client.get("hello.json") + self.assertEqual(result['message'], "Hello, world!") + self.assertEqual(result['result']['__route__'], "/v%d/hello.json" % api_version) + + def test_unspecified_api_version(self): + # If the API version is unspecified, we get a generic hello world message + client = self.create_client_from_base(api_version=None) + result = client.get("hello.json") + self.assertEqual(result['message'], "Hello, world!") + self.assertEqual(result['result']['__route__'], "/hello.json") + + def test_future_api_version(self): + # If the API version is unsupported, we should get an error + # (it's a ValueError, since the returned 404 HTML page is not valid JSON) + client = self.create_client_from_base(api_version=openphoto.LATEST_API_VERSION + 1) + with self.assertRaises(ValueError): + client.get("hello.json") From a477dc796c5d7b5a2443142f8f1b1aaba584ccb5 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 20 Apr 2013 11:20:22 +0100 Subject: [PATCH 13/25] Remove unneeded LOG_FILENAME variable --- tests/test_base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_base.py b/tests/test_base.py index c184746..34a99f4 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -26,7 +26,6 @@ class TestBase(unittest.TestCase): unittest.TestCase.__init__(self, *args, **kwds) self.photos = [] - LOG_FILENAME = "tests.log" logging.basicConfig(filename="tests.log", filemode="w", format="%(message)s", From 9cbbd1bd47066ca8f7ed73017d929d3b3c6721ff Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 20 Apr 2013 11:48:58 +0100 Subject: [PATCH 14/25] Run all tests at all API versions --- tests/api_versions/__init__.py | 0 tests/api_versions/test_v1.py | 10 ++++++++++ tests/api_versions/test_v2.py | 10 ++++++++++ tests/test_albums.py | 1 + tests/test_base.py | 12 ++++++++++-- tests/test_framework.py | 2 ++ tests/test_photos.py | 2 ++ tests/test_tags.py | 2 ++ 8 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 tests/api_versions/__init__.py create mode 100644 tests/api_versions/test_v1.py create mode 100644 tests/api_versions/test_v2.py diff --git a/tests/api_versions/__init__.py b/tests/api_versions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/api_versions/test_v1.py b/tests/api_versions/test_v1.py new file mode 100644 index 0000000..92baabb --- /dev/null +++ b/tests/api_versions/test_v1.py @@ -0,0 +1,10 @@ +from tests import test_albums, test_photos, test_tags + +class TestAlbumsV1(test_albums.TestAlbums): + api_version = 1 + +class TestPhotosV1(test_photos.TestPhotos): + api_version = 1 + +class TestTagsV1(test_tags.TestTags): + api_version = 1 diff --git a/tests/api_versions/test_v2.py b/tests/api_versions/test_v2.py new file mode 100644 index 0000000..a6cfa4e --- /dev/null +++ b/tests/api_versions/test_v2.py @@ -0,0 +1,10 @@ +from tests import test_albums, test_photos, test_tags + +class TestAlbumsV2(test_albums.TestAlbums): + api_version = 2 + +class TestPhotosV2(test_photos.TestPhotos): + api_version = 2 + +class TestTagsV2(test_tags.TestTags): + api_version = 2 diff --git a/tests/test_albums.py b/tests/test_albums.py index 563660f..bc2de51 100644 --- a/tests/test_albums.py +++ b/tests/test_albums.py @@ -3,6 +3,7 @@ import openphoto import test_base class TestAlbums(test_base.TestBase): + testcase_name = "album API" def test_create_delete(self): """ Create an album then delete it """ diff --git a/tests/test_base.py b/tests/test_base.py index ad425b1..7563c46 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -22,6 +22,8 @@ class TestBase(unittest.TestCase): TEST_TAG = "test_tag" TEST_ALBUM = "test_album" MAXIMUM_TEST_PHOTOS = 4 # Never have more the 4 photos on the test server + testcase_name = "(unknown testcase)" + api_version = None def __init__(self, *args, **kwds): unittest.TestCase.__init__(self, *args, **kwds) @@ -36,9 +38,15 @@ class TestBase(unittest.TestCase): @classmethod def setUpClass(cls): """ Ensure there is nothing on the server before running any tests """ + if cls.api_version is None: + print "\nTesting Latest %s" % cls.testcase_name + else: + print "\nTesting %s v%d" % (cls.testcase_name, cls.api_version) + cls.client = openphoto.OpenPhoto(tokens.host, - tokens.consumer_key, tokens.consumer_secret, - tokens.token, tokens.token_secret) + tokens.consumer_key, tokens.consumer_secret, + tokens.token, tokens.token_secret, + cls.api_version) if cls.client.photos.list() != []: raise ValueError("The test server (%s) contains photos. " diff --git a/tests/test_framework.py b/tests/test_framework.py index 2627e9e..136e0ca 100644 --- a/tests/test_framework.py +++ b/tests/test_framework.py @@ -4,6 +4,8 @@ import openphoto import test_base class TestFramework(test_base.TestBase): + testcase_name = "framework" + def setUp(self): """Override the default setUp, since we don't need a populated database""" logging.info("\nRunning %s..." % self.id()) diff --git a/tests/test_photos.py b/tests/test_photos.py index 04e216d..0704220 100644 --- a/tests/test_photos.py +++ b/tests/test_photos.py @@ -3,6 +3,8 @@ import openphoto import test_base class TestPhotos(test_base.TestBase): + testcase_name = "photo API" + def test_delete_upload(self): """ Test photo deletion and upload """ # Delete one photo using the OpenPhoto class, passing in the id diff --git a/tests/test_tags.py b/tests/test_tags.py index 7f06ed2..aa3a821 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -3,6 +3,8 @@ import openphoto import test_base class TestTags(test_base.TestBase): + testcase_name = "tag API" + @unittest.expectedFailure # Tag create fails - Issue #927 # NOTE: the below has not been tested/debugged, since it fails at the first step def test_create_delete(self, tag_name="create_tag"): From fc234096f262cec0727ce41353526edef85994d4 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 20 Apr 2013 12:06:35 +0100 Subject: [PATCH 15/25] Handle case where tag count is returned as string (as was the case in APIv1) --- tests/test_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_base.py b/tests/test_base.py index 7563c46..f70f169 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -86,7 +86,7 @@ class TestBase(unittest.TestCase): self.tags = self.client.tags.list() if (len(self.tags) != 1 or self.tags[0].id != self.TEST_TAG or - self.tags[0].count != 3): + str(self.tags[0].count) != "3"): print "[Regenerating Tags]" self._delete_all() self._create_test_photos() From 7501c8ca775728ec9076690d3cc346d78ca97040 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 20 Apr 2013 12:12:40 +0100 Subject: [PATCH 16/25] next/previous fields are lists in APIv2, but single photo ids in APIv1. --- openphoto/objects.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openphoto/objects.py b/openphoto/objects.py index d85a780..78af7be 100644 --- a/openphoto/objects.py +++ b/openphoto/objects.py @@ -87,13 +87,23 @@ class Photo(OpenPhotoObject): **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._openphoto, 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._openphoto, photo)) + return value def transform(self, **kwds): From 32ebfc511bd17af084222e3b06fa5d671d76c9a3 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 20 Apr 2013 13:45:24 +0200 Subject: [PATCH 17/25] Update README with API versioning documentation --- README.markdown | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.markdown b/README.markdown index fda226d..66c8fe7 100644 --- a/README.markdown +++ b/README.markdown @@ -41,6 +41,17 @@ The OpenPhoto Python class hierarchy mirrors the [OpenPhoto API](http://theopenp * client.photos.list() -> /photos/list.json * photos[0].update() -> /photo/<id>/update.json + +### API Versioning + +It may be useful to lock your application to a particular version of the OpenPhoto API. +This ensures that future API updates won't cause unexpected breakages. + +To do this, add the optional ```api_version``` parameter when creating the client object: + + from openphoto import OpenPhoto + client = OpenPhoto(host, consumerKey, consumerSecret, token, tokenSecret, api_version=2) + ---------------------------------------- From 7fad16d6861ce26546c5653ec2c91f0fde1e0de2 Mon Sep 17 00:00:00 2001 From: Pete Date: Sat, 20 Apr 2013 18:52:42 +0100 Subject: [PATCH 18/25] Use the OAuth2 client for multipart POSTing - no need for urllib2 --- openphoto/openphoto_http.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openphoto/openphoto_http.py b/openphoto/openphoto_http.py index 59431f6..b728ac1 100644 --- a/openphoto/openphoto_http.py +++ b/openphoto/openphoto_http.py @@ -1,7 +1,6 @@ import oauth2 as oauth import urlparse import urllib -import urllib2 import httplib2 import logging try: @@ -90,11 +89,11 @@ class OpenPhotoHttp: # Parameters must be signed and encoded into the multipart body params = self._sign_params(client, url, params) headers, body = encode_multipart_formdata(params, files) - request = urllib2.Request(url, body, headers) - content = urllib2.urlopen(request).read() else: body = urllib.urlencode(params) - _, content = client.request(url, "POST", body) + headers = None + + _, content = client.request(url, "POST", body, headers) # TODO: Don't log file data in multipart forms self._logger.info("============================") From e4e24755dd6dc78e26b3653a5a01d3a1f3c2055f Mon Sep 17 00:00:00 2001 From: Pete Date: Sat, 20 Apr 2013 18:53:14 +0100 Subject: [PATCH 19/25] Check that upload parameters are processed correctly --- tests/test_photos.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_photos.py b/tests/test_photos.py index 04e216d..ce9959f 100644 --- a/tests/test_photos.py +++ b/tests/test_photos.py @@ -23,9 +23,11 @@ class TestPhotos(test_base.TestBase): self.client.photo.upload_encoded("tests/test_photo3.jpg", title=self.TEST_TITLE) - # Check there are now three photos + # Check there are now three photos with the correct titles self.photos = self.client.photos.list() self.assertEqual(len(self.photos), 3) + for photo in self.photos: + self.assertEqual(photo.title, self.TEST_TITLE) # Check that the upload return value was correct pathOriginals = [photo.pathOriginal for photo in self.photos] From 44146b001dee4a70c58905308ea2ecdd045fffd1 Mon Sep 17 00:00:00 2001 From: Pete Date: Sat, 20 Apr 2013 19:06:55 +0100 Subject: [PATCH 20/25] Improve http logging, and don't log multipart upload contents --- openphoto/openphoto_http.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openphoto/openphoto_http.py b/openphoto/openphoto_http.py index b728ac1..56d944d 100644 --- a/openphoto/openphoto_http.py +++ b/openphoto/openphoto_http.py @@ -87,19 +87,19 @@ class OpenPhotoHttp: if files: # Parameters must be signed and encoded into the multipart body - params = self._sign_params(client, url, params) - headers, body = encode_multipart_formdata(params, files) + signed_params = self._sign_params(client, url, params) + headers, body = encode_multipart_formdata(signed_params, files) else: body = urllib.urlencode(params) headers = None _, content = client.request(url, "POST", body, headers) - # TODO: Don't log file data in multipart forms self._logger.info("============================") self._logger.info("POST %s" % url) - if body: - self._logger.info(body) + self._logger.info("params: %s" % repr(params)) + if files: + self._logger.info("files: %s" % repr(files)) self._logger.info("---") self._logger.info(content) From 35d091d2a15335dbb0c57f25ec950d936df4bfa4 Mon Sep 17 00:00:00 2001 From: Pete Date: Sat, 20 Apr 2013 20:02:43 +0100 Subject: [PATCH 21/25] Check the HTTP response code if the OpenPhoto JSON cannot be decoded. --- openphoto/errors.py | 4 +++ openphoto/openphoto_http.py | 49 ++++++++++++++++++++----------------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/openphoto/errors.py b/openphoto/errors.py index 25a0b24..218fd35 100644 --- a/openphoto/errors.py +++ b/openphoto/errors.py @@ -6,6 +6,10 @@ class OpenPhotoDuplicateError(OpenPhotoError): """ Indicates that an upload operation failed due to a duplicate photo """ pass +class OpenPhoto404Error(Exception): + """ Indicates that an Http 404 error code was received (resource not found) """ + pass + class NotImplementedError(OpenPhotoError): """ Indicates that the API function has not yet been coded - please help! """ pass diff --git a/openphoto/openphoto_http.py b/openphoto/openphoto_http.py index 56d944d..b796fb0 100644 --- a/openphoto/openphoto_http.py +++ b/openphoto/openphoto_http.py @@ -50,7 +50,7 @@ class OpenPhotoHttp: else: client = httplib2.Http() - _, content = client.request(url, "GET") + response, content = client.request(url, "GET") self._logger.info("============================") self._logger.info("GET %s" % url) @@ -59,11 +59,10 @@ class OpenPhotoHttp: self.last_url = url self.last_params = params - self.last_response = content + self.last_response = (response, content) if process_response: - return self._process_response(content) - return response + return self._process_response(response, content) else: return content @@ -93,7 +92,7 @@ class OpenPhotoHttp: body = urllib.urlencode(params) headers = None - _, content = client.request(url, "POST", body, headers) + response, content = client.request(url, "POST", body, headers) self._logger.info("============================") self._logger.info("POST %s" % url) @@ -105,10 +104,10 @@ class OpenPhotoHttp: self.last_url = url self.last_params = params - self.last_response = content + self.last_response = (response, content) if process_response: - return self._process_response(content) + return self._process_response(response, content) else: return content @@ -155,26 +154,32 @@ class OpenPhotoHttp: return processed_params @staticmethod - def _process_response(content): + def _process_response(response, content): """ Decodes the JSON response, returning a dict. Raises an exception if an invalid response code is received. """ - response = json.loads(content) + try: + json_response = json.loads(content) + code = json_response["code"] + message = json_response["message"] + except ValueError, KeyError: + # Response wasn't OpenPhoto JSON - check the HTTP status code + if 200 <= response.status < 300: + # Status code was valid, so just reraise the exception + raise + elif response.status == 404: + raise OpenPhoto404Error("HTTP Error %d: %s" % (response.status, response.reason)) + else: + raise OpenPhotoError("HTTP Error %d: %s" % (response.status, response.reason)) - if response["code"] >= 200 and response["code"] < 300: - # Valid response code - return response - - error_message = "Code %d: %s" % (response["code"], - response["message"]) - - # Special case for a duplicate photo error - if (response["code"] == DUPLICATE_RESPONSE["code"] and - DUPLICATE_RESPONSE["message"] in response["message"]): - raise OpenPhotoDuplicateError(error_message) - - raise OpenPhotoError(error_message) + if 200 <= code < 300: + return json_response + elif (code == DUPLICATE_RESPONSE["code"] and + DUPLICATE_RESPONSE["message"] in message): + raise OpenPhotoDuplicateError("Code %d: %s" % (code, message)) + else: + raise OpenPhotoError("Code %d: %s" % (code, message)) @staticmethod def _result_to_list(result): From 35a271c25bcae8aeb23bcfe48953382ac53d43fc Mon Sep 17 00:00:00 2001 From: Pete Date: Sat, 20 Apr 2013 20:36:29 +0100 Subject: [PATCH 22/25] Improve POST logging - don't log file contents. --- openphoto/openphoto_http.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openphoto/openphoto_http.py b/openphoto/openphoto_http.py index 59431f6..a60e84c 100644 --- a/openphoto/openphoto_http.py +++ b/openphoto/openphoto_http.py @@ -88,19 +88,20 @@ class OpenPhotoHttp: if files: # Parameters must be signed and encoded into the multipart body - params = self._sign_params(client, url, params) - headers, body = encode_multipart_formdata(params, files) + signed_params = self._sign_params(client, url, params) + headers, body = encode_multipart_formdata(signed_params, files) request = urllib2.Request(url, body, headers) content = urllib2.urlopen(request).read() else: body = urllib.urlencode(params) _, content = client.request(url, "POST", body) - # TODO: Don't log file data in multipart forms self._logger.info("============================") self._logger.info("POST %s" % url) - if body: - self._logger.info(body) + if params: + self._logger.info("params: %s" % repr(params)) + if files: + self._logger.info("files: %s" % repr(files)) self._logger.info("---") self._logger.info(content) From 735daab58edc61260948d8f052f47be22c755bf3 Mon Sep 17 00:00:00 2001 From: Pete Date: Sun, 21 Apr 2013 16:23:25 +0100 Subject: [PATCH 23/25] Revert 35a271c25bcae8aeb23bcfe48953382ac53d43fc, since this has been applied on the http_improvements branch --- openphoto/openphoto_http.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openphoto/openphoto_http.py b/openphoto/openphoto_http.py index a60e84c..5bb280b 100644 --- a/openphoto/openphoto_http.py +++ b/openphoto/openphoto_http.py @@ -88,20 +88,19 @@ class OpenPhotoHttp: if files: # Parameters must be signed and encoded into the multipart body - signed_params = self._sign_params(client, url, params) - headers, body = encode_multipart_formdata(signed_params, files) + params = self._sign_params(client, url, params) + headers, body = encode_multipart_formdata(params, files) request = urllib2.Request(url, body, headers) content = urllib2.urlopen(request).read() else: body = urllib.urlencode(params) _, content = client.request(url, "POST", body) + # TODO: Don't log file data in multipart forms self._logger.info("============================") self._logger.info("POST %s" % url) - if params: - self._logger.info("params: %s" % repr(params)) - if files: - self._logger.info("files: %s" % repr(files)) + if body: + self._logger.info(body) self._logger.info("---") self._logger.info(content) From 2ddcc6978ecbe5980d4addd894b1ac538862fc7c Mon Sep 17 00:00:00 2001 From: Pete Date: Sun, 21 Apr 2013 16:25:01 +0100 Subject: [PATCH 24/25] Comment whitespace --- openphoto/openphoto_http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openphoto/openphoto_http.py b/openphoto/openphoto_http.py index 5bb280b..59431f6 100644 --- a/openphoto/openphoto_http.py +++ b/openphoto/openphoto_http.py @@ -96,7 +96,7 @@ class OpenPhotoHttp: body = urllib.urlencode(params) _, content = client.request(url, "POST", body) - # TODO: Don't log file data in multipart forms + # TODO: Don't log file data in multipart forms self._logger.info("============================") self._logger.info("POST %s" % url) if body: From 4658b5c53a00b3dd3211e1e5576431fc7cf12f2b Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 28 Apr 2013 18:38:56 +0100 Subject: [PATCH 25/25] Error in future API test now correctly raises a 404 exception. --- tests/test_framework.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_framework.py b/tests/test_framework.py index 136e0ca..4d2b13e 100644 --- a/tests/test_framework.py +++ b/tests/test_framework.py @@ -44,5 +44,5 @@ class TestFramework(test_base.TestBase): # If the API version is unsupported, we should get an error # (it's a ValueError, since the returned 404 HTML page is not valid JSON) client = self.create_client_from_base(api_version=openphoto.LATEST_API_VERSION + 1) - with self.assertRaises(ValueError): + with self.assertRaises(openphoto.OpenPhoto404Error): client.get("hello.json")