Merge branch 'master' into class_config
Conflicts: openphoto/__init__.py openphoto/openphoto_http.py tests/test_base.py
This commit is contained in:
commit
42999f80f5
17 changed files with 293 additions and 109 deletions
|
@ -59,6 +59,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``
|
||||
|
||||
<a name="api_versioning"></a>
|
||||
### 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(api_version=2)
|
||||
|
||||
----------------------------------------
|
||||
|
||||
<a name="cli"></a>
|
||||
|
|
|
@ -4,20 +4,27 @@ import api_photo
|
|||
import api_tag
|
||||
import api_album
|
||||
|
||||
LATEST_API_VERSION = 2
|
||||
|
||||
class OpenPhoto(OpenPhotoHttp):
|
||||
"""
|
||||
Client library for OpenPhoto
|
||||
If no parameters are specified, config is loaded from the default
|
||||
location (~/.config/openphoto/default).
|
||||
The config_file parameter is used to specify an alternate config file.
|
||||
If the host parameter is specified, no config file is loaded.
|
||||
If the host parameter is specified, no config file is loaded and
|
||||
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, config_file=None, host=None,
|
||||
consumer_key='', consumer_secret='',
|
||||
token='', token_secret=''):
|
||||
token='', token_secret='',
|
||||
api_version=None):
|
||||
OpenPhotoHttp.__init__(self, config_file, 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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
@ -39,10 +43,7 @@ class ApiAlbum:
|
|||
if not isinstance(album, Album):
|
||||
album = Album(self._client, {"id": album})
|
||||
album.update(**kwds)
|
||||
|
||||
# Don't return the album, since the API currently doesn't give us the modified album
|
||||
# TODO: Uncomment the following once frontend issue #937 is resolved
|
||||
# return album
|
||||
return album
|
||||
|
||||
def view(self, album, **kwds):
|
||||
"""
|
||||
|
|
|
@ -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 """
|
||||
|
@ -91,4 +104,11 @@ 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)
|
||||
return photo
|
||||
|
|
|
@ -15,15 +15,18 @@ 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 """
|
||||
"""
|
||||
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 """
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import urllib
|
||||
from errors import *
|
||||
|
||||
class OpenPhotoObject:
|
||||
|
@ -39,9 +40,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 """
|
||||
|
@ -82,28 +88,48 @@ 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):
|
||||
raise NotImplementedError()
|
||||
|
||||
"""
|
||||
Performs transformation specified in **kwds
|
||||
Example: transform(rotate=90)
|
||||
"""
|
||||
new_dict = self._openphoto.post("/photo/%s/transform.json" % self.id,
|
||||
**kwds)["result"]
|
||||
self._replace_fields(new_dict)
|
||||
|
||||
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" % urllib.quote(self.id), **kwds)["result"]
|
||||
self._replace_fields({})
|
||||
return result
|
||||
|
||||
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)
|
||||
|
||||
|
@ -125,9 +151,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()
|
||||
|
@ -142,14 +173,8 @@ class Album(OpenPhotoObject):
|
|||
""" Update this album with the specified parameters """
|
||||
new_dict = self._openphoto.post("/album/%s/update.json" % self.id,
|
||||
**kwds)["result"]
|
||||
|
||||
# Since the API doesn't give us the modified album, we need to
|
||||
# update our fields based on the kwds that were sent
|
||||
self._set_fields(kwds)
|
||||
|
||||
# Replace the above line with the below once frontend issue #937 is resolved
|
||||
# self._set_fields(new_dict)
|
||||
# self._update_fields_with_objects()
|
||||
self._replace_fields(new_dict)
|
||||
self._update_fields_with_objects()
|
||||
|
||||
def view(self, **kwds):
|
||||
"""
|
||||
|
|
|
@ -2,7 +2,6 @@ import os
|
|||
import oauth2 as oauth
|
||||
import urlparse
|
||||
import urllib
|
||||
import urllib2
|
||||
import httplib2
|
||||
import logging
|
||||
import StringIO
|
||||
|
@ -25,11 +24,16 @@ class OpenPhotoHttp:
|
|||
If no parameters are specified, config is loaded from the default
|
||||
location (~/.config/openphoto/default).
|
||||
The config_file parameter is used to specify an alternate config file.
|
||||
If the host parameter is specified, no config file is loaded.
|
||||
If the host parameter is specified, no config file is loaded and
|
||||
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, config_file=None, host=None,
|
||||
consumer_key='', consumer_secret='',
|
||||
token='', token_secret=''):
|
||||
token='', token_secret='', api_version=None):
|
||||
self._api_version = api_version
|
||||
|
||||
self._logger = logging.getLogger("openphoto")
|
||||
|
||||
|
@ -60,11 +64,18 @@ class OpenPhotoHttp:
|
|||
"""
|
||||
Performs an HTTP GET from the specified endpoint (API path),
|
||||
passing parameters if given.
|
||||
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:
|
||||
|
@ -74,7 +85,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)
|
||||
|
@ -83,11 +94,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
|
||||
|
||||
|
@ -95,13 +105,20 @@ class OpenPhotoHttp:
|
|||
"""
|
||||
Performs an HTTP POST to the specified endpoint (API path),
|
||||
passing parameters if given.
|
||||
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")
|
||||
|
||||
|
@ -111,28 +128,28 @@ 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)
|
||||
request = urllib2.Request(url, body, headers)
|
||||
content = urllib2.urlopen(request).read()
|
||||
signed_params = self._sign_params(client, url, params)
|
||||
headers, body = encode_multipart_formdata(signed_params, files)
|
||||
else:
|
||||
body = urllib.urlencode(params)
|
||||
_, content = client.request(url, "POST", body)
|
||||
headers = None
|
||||
|
||||
response, 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)
|
||||
|
||||
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
|
||||
|
||||
|
@ -179,26 +196,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):
|
||||
|
|
|
@ -9,7 +9,7 @@ A computer, Python 2.7 and an empty OpenPhoto test host.
|
|||
|
||||
---------------------------------------
|
||||
<a name="setup"></a>
|
||||
### Setting up
|
||||
### Setting up
|
||||
|
||||
Create a ``~/.config/openphoto/test`` config file containing the following:
|
||||
|
||||
|
@ -38,7 +38,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".
|
||||
|
||||
---------------------------------------
|
||||
<a name="test_details"></a>
|
||||
|
|
0
tests/api_versions/__init__.py
Normal file
0
tests/api_versions/__init__.py
Normal file
10
tests/api_versions/test_v1.py
Normal file
10
tests/api_versions/test_v1.py
Normal file
|
@ -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
|
10
tests/api_versions/test_v2.py
Normal file
10
tests/api_versions/test_v2.py
Normal file
|
@ -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
|
|
@ -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 """
|
||||
|
@ -15,13 +16,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()])
|
||||
|
||||
|
|
|
@ -8,12 +8,14 @@ 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
|
||||
config_file = os.getenv("OPENPHOTO_TEST_CONFIG", "test")
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
unittest.TestCase.__init__(self, *args, **kwds)
|
||||
self.photos = []
|
||||
|
||||
LOG_FILENAME = "tests.log"
|
||||
logging.basicConfig(filename="tests.log",
|
||||
filemode="w",
|
||||
format="%(message)s",
|
||||
|
@ -22,8 +24,13 @@ class TestBase(unittest.TestCase):
|
|||
@classmethod
|
||||
def setUpClass(cls):
|
||||
""" Ensure there is nothing on the server before running any tests """
|
||||
config_file = os.getenv("OPENPHOTO_TEST_CONFIG", "test")
|
||||
cls.client = openphoto.OpenPhoto(config_file=config_file)
|
||||
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(config_file=cls.config_file,
|
||||
api_version=cls.api_version)
|
||||
|
||||
if cls.client.photos.list() != []:
|
||||
raise ValueError("The test server (%s) contains photos. "
|
||||
|
@ -63,7 +70,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()
|
||||
|
|
44
tests/test_framework.py
Normal file
44
tests/test_framework.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
import unittest
|
||||
import logging
|
||||
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())
|
||||
|
||||
def test_api_version_zero(self):
|
||||
# API v0 has a special hello world message
|
||||
client = openphoto.OpenPhoto(config_file=self.config_file,
|
||||
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 = openphoto.OpenPhoto(config_file=self.config_file,
|
||||
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 = openphoto.OpenPhoto(config_file=self.config_file,
|
||||
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 = openphoto.OpenPhoto(config_file=self.config_file,
|
||||
api_version=openphoto.LATEST_API_VERSION + 1)
|
||||
with self.assertRaises(openphoto.OpenPhoto404Error):
|
||||
client.get("hello.json")
|
|
@ -3,14 +3,16 @@ 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
|
||||
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(), [])
|
||||
|
@ -23,16 +25,18 @@ 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]
|
||||
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()
|
||||
|
@ -147,6 +151,12 @@ 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)
|
||||
self.assertEqual(photo.rotation, "90")
|
||||
|
||||
# Do the same using the Photo object directly
|
||||
photo.transform(rotate=90)
|
||||
self.assertEqual(photo.rotation, "180")
|
||||
|
|
|
@ -3,31 +3,35 @@ 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"):
|
||||
testcase_name = "tag API"
|
||||
|
||||
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.assertTrue(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)
|
||||
tag.delete()
|
||||
# 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]
|
||||
self.assertTrue(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
|
||||
# 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
|
||||
|
@ -57,15 +61,17 @@ 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")
|
||||
|
||||
# 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")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue