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``
|
* ``client.photos.list() -> /photos/list.json``
|
||||||
* ``photos[0].update() -> /photo/<id>/update.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>
|
<a name="cli"></a>
|
||||||
|
|
|
@ -4,20 +4,27 @@ import api_photo
|
||||||
import api_tag
|
import api_tag
|
||||||
import api_album
|
import api_album
|
||||||
|
|
||||||
|
LATEST_API_VERSION = 2
|
||||||
|
|
||||||
class OpenPhoto(OpenPhotoHttp):
|
class OpenPhoto(OpenPhotoHttp):
|
||||||
"""
|
"""
|
||||||
Client library for OpenPhoto
|
Client library for OpenPhoto
|
||||||
If no parameters are specified, config is loaded from the default
|
If no parameters are specified, config is loaded from the default
|
||||||
location (~/.config/openphoto/default).
|
location (~/.config/openphoto/default).
|
||||||
The config_file parameter is used to specify an alternate config file.
|
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,
|
def __init__(self, config_file=None, host=None,
|
||||||
consumer_key='', consumer_secret='',
|
consumer_key='', consumer_secret='',
|
||||||
token='', token_secret=''):
|
token='', token_secret='',
|
||||||
|
api_version=None):
|
||||||
OpenPhotoHttp.__init__(self, config_file, host,
|
OpenPhotoHttp.__init__(self, config_file, host,
|
||||||
consumer_key, consumer_secret,
|
consumer_key, consumer_secret,
|
||||||
token, token_secret)
|
token, token_secret, api_version)
|
||||||
|
|
||||||
self.photos = api_photo.ApiPhotos(self)
|
self.photos = api_photo.ApiPhotos(self)
|
||||||
self.photo = api_photo.ApiPhoto(self)
|
self.photo = api_photo.ApiPhoto(self)
|
||||||
|
|
|
@ -20,10 +20,14 @@ class ApiAlbum:
|
||||||
return Album(self._client, result)
|
return Album(self._client, result)
|
||||||
|
|
||||||
def delete(self, album, **kwds):
|
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):
|
if not isinstance(album, Album):
|
||||||
album = Album(self._client, {"id": album})
|
album = Album(self._client, {"id": album})
|
||||||
album.delete(**kwds)
|
return album.delete(**kwds)
|
||||||
|
|
||||||
def form(self, album, **kwds):
|
def form(self, album, **kwds):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
@ -39,10 +43,7 @@ class ApiAlbum:
|
||||||
if not isinstance(album, Album):
|
if not isinstance(album, Album):
|
||||||
album = Album(self._client, {"id": album})
|
album = Album(self._client, {"id": album})
|
||||||
album.update(**kwds)
|
album.update(**kwds)
|
||||||
|
return album
|
||||||
# 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
|
|
||||||
|
|
||||||
def view(self, album, **kwds):
|
def view(self, album, **kwds):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -14,25 +14,38 @@ class ApiPhotos:
|
||||||
return [Photo(self._client, photo) for photo in photos]
|
return [Photo(self._client, photo) for photo in photos]
|
||||||
|
|
||||||
def update(self, photos, **kwds):
|
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"]:
|
if not self._client.post("/photos/update.json", ids=photos, **kwds)["result"]:
|
||||||
raise OpenPhotoError("Update response returned False")
|
raise OpenPhotoError("Update response returned False")
|
||||||
|
return True
|
||||||
|
|
||||||
def delete(self, photos, **kwds):
|
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"]:
|
if not self._client.post("/photos/delete.json", ids=photos, **kwds)["result"]:
|
||||||
raise OpenPhotoError("Delete response returned False")
|
raise OpenPhotoError("Delete response returned False")
|
||||||
|
return True
|
||||||
|
|
||||||
class ApiPhoto:
|
class ApiPhoto:
|
||||||
def __init__(self, client):
|
def __init__(self, client):
|
||||||
self._client = client
|
self._client = client
|
||||||
|
|
||||||
def delete(self, photo, **kwds):
|
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):
|
if not isinstance(photo, Photo):
|
||||||
photo = Photo(self._client, {"id": photo})
|
photo = Photo(self._client, {"id": photo})
|
||||||
photo.delete(**kwds)
|
return photo.delete(**kwds)
|
||||||
|
|
||||||
def edit(self, photo, **kwds):
|
def edit(self, photo, **kwds):
|
||||||
""" Returns an HTML form to edit a photo """
|
""" Returns an HTML form to edit a photo """
|
||||||
|
@ -91,4 +104,11 @@ class ApiPhoto:
|
||||||
return photo.next_previous(**kwds)
|
return photo.next_previous(**kwds)
|
||||||
|
|
||||||
def transform(self, photo, **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
|
self._client = client
|
||||||
|
|
||||||
def create(self, tag, **kwds):
|
def create(self, tag, **kwds):
|
||||||
""" Create a new tag and return it """
|
""" Create a new tag. The API returns true if the tag was sucessfully created """
|
||||||
result = self._client.post("/tag/create.json", tag=tag, **kwds)["result"]
|
return self._client.post("/tag/create.json", tag=tag, **kwds)["result"]
|
||||||
return Tag(self._client, result)
|
|
||||||
|
|
||||||
def delete(self, tag, **kwds):
|
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):
|
if not isinstance(tag, Tag):
|
||||||
tag = Tag(self._client, {"id": tag})
|
tag = Tag(self._client, {"id": tag})
|
||||||
tag.delete(**kwds)
|
return tag.delete(**kwds)
|
||||||
|
|
||||||
def update(self, tag, **kwds):
|
def update(self, tag, **kwds):
|
||||||
""" Update a tag """
|
""" Update a tag """
|
||||||
|
|
|
@ -6,6 +6,10 @@ class OpenPhotoDuplicateError(OpenPhotoError):
|
||||||
""" Indicates that an upload operation failed due to a duplicate photo """
|
""" Indicates that an upload operation failed due to a duplicate photo """
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class OpenPhoto404Error(Exception):
|
||||||
|
""" Indicates that an Http 404 error code was received (resource not found) """
|
||||||
|
pass
|
||||||
|
|
||||||
class NotImplementedError(OpenPhotoError):
|
class NotImplementedError(OpenPhotoError):
|
||||||
""" Indicates that the API function has not yet been coded - please help! """
|
""" Indicates that the API function has not yet been coded - please help! """
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import urllib
|
||||||
from errors import *
|
from errors import *
|
||||||
|
|
||||||
class OpenPhotoObject:
|
class OpenPhotoObject:
|
||||||
|
@ -39,9 +40,14 @@ class OpenPhotoObject:
|
||||||
|
|
||||||
class Photo(OpenPhotoObject):
|
class Photo(OpenPhotoObject):
|
||||||
def delete(self, **kwds):
|
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({})
|
self._replace_fields({})
|
||||||
|
return result
|
||||||
|
|
||||||
def edit(self, **kwds):
|
def edit(self, **kwds):
|
||||||
""" Returns an HTML form to edit the photo """
|
""" Returns an HTML form to edit the photo """
|
||||||
|
@ -82,28 +88,48 @@ class Photo(OpenPhotoObject):
|
||||||
**kwds)["result"]
|
**kwds)["result"]
|
||||||
value = {}
|
value = {}
|
||||||
if "next" in result:
|
if "next" in result:
|
||||||
|
# Workaround for APIv1
|
||||||
|
if not isinstance(result["next"], list):
|
||||||
|
result["next"] = [result["next"]]
|
||||||
|
|
||||||
value["next"] = []
|
value["next"] = []
|
||||||
for photo in result["next"]:
|
for photo in result["next"]:
|
||||||
value["next"].append(Photo(self._openphoto, photo))
|
value["next"].append(Photo(self._openphoto, photo))
|
||||||
|
|
||||||
if "previous" in result:
|
if "previous" in result:
|
||||||
|
# Workaround for APIv1
|
||||||
|
if not isinstance(result["previous"], list):
|
||||||
|
result["previous"] = [result["previous"]]
|
||||||
|
|
||||||
value["previous"] = []
|
value["previous"] = []
|
||||||
for photo in result["previous"]:
|
for photo in result["previous"]:
|
||||||
value["previous"].append(Photo(self._openphoto, photo))
|
value["previous"].append(Photo(self._openphoto, photo))
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def transform(self, **kwds):
|
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):
|
class Tag(OpenPhotoObject):
|
||||||
def delete(self, **kwds):
|
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({})
|
self._replace_fields({})
|
||||||
|
return result
|
||||||
|
|
||||||
def update(self, **kwds):
|
def update(self, **kwds):
|
||||||
""" Update this tag with the specified parameters """
|
""" 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"]
|
**kwds)["result"]
|
||||||
self._replace_fields(new_dict)
|
self._replace_fields(new_dict)
|
||||||
|
|
||||||
|
@ -125,9 +151,14 @@ class Album(OpenPhotoObject):
|
||||||
self.photos[i] = Photo(self._openphoto, photo)
|
self.photos[i] = Photo(self._openphoto, photo)
|
||||||
|
|
||||||
def delete(self, **kwds):
|
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({})
|
self._replace_fields({})
|
||||||
|
return result
|
||||||
|
|
||||||
def form(self, **kwds):
|
def form(self, **kwds):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
@ -142,14 +173,8 @@ class Album(OpenPhotoObject):
|
||||||
""" Update this album with the specified parameters """
|
""" Update this album with the specified parameters """
|
||||||
new_dict = self._openphoto.post("/album/%s/update.json" % self.id,
|
new_dict = self._openphoto.post("/album/%s/update.json" % self.id,
|
||||||
**kwds)["result"]
|
**kwds)["result"]
|
||||||
|
self._replace_fields(new_dict)
|
||||||
# Since the API doesn't give us the modified album, we need to
|
self._update_fields_with_objects()
|
||||||
# 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()
|
|
||||||
|
|
||||||
def view(self, **kwds):
|
def view(self, **kwds):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -2,7 +2,6 @@ import os
|
||||||
import oauth2 as oauth
|
import oauth2 as oauth
|
||||||
import urlparse
|
import urlparse
|
||||||
import urllib
|
import urllib
|
||||||
import urllib2
|
|
||||||
import httplib2
|
import httplib2
|
||||||
import logging
|
import logging
|
||||||
import StringIO
|
import StringIO
|
||||||
|
@ -25,11 +24,16 @@ class OpenPhotoHttp:
|
||||||
If no parameters are specified, config is loaded from the default
|
If no parameters are specified, config is loaded from the default
|
||||||
location (~/.config/openphoto/default).
|
location (~/.config/openphoto/default).
|
||||||
The config_file parameter is used to specify an alternate config file.
|
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,
|
def __init__(self, config_file=None, host=None,
|
||||||
consumer_key='', consumer_secret='',
|
consumer_key='', consumer_secret='',
|
||||||
token='', token_secret=''):
|
token='', token_secret='', api_version=None):
|
||||||
|
self._api_version = api_version
|
||||||
|
|
||||||
self._logger = logging.getLogger("openphoto")
|
self._logger = logging.getLogger("openphoto")
|
||||||
|
|
||||||
|
@ -60,11 +64,18 @@ class OpenPhotoHttp:
|
||||||
"""
|
"""
|
||||||
Performs an HTTP GET from the specified endpoint (API path),
|
Performs an HTTP GET from the specified endpoint (API path),
|
||||||
passing parameters if given.
|
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
|
Returns the decoded JSON dictionary, and raises exceptions if an
|
||||||
error code is received.
|
error code is received.
|
||||||
Returns the raw response if process_response=False
|
Returns the raw response if process_response=False
|
||||||
"""
|
"""
|
||||||
params = self._process_params(params)
|
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, '',
|
url = urlparse.urlunparse(('http', self._host, endpoint, '',
|
||||||
urllib.urlencode(params), ''))
|
urllib.urlencode(params), ''))
|
||||||
if self._consumer_key:
|
if self._consumer_key:
|
||||||
|
@ -74,7 +85,7 @@ class OpenPhotoHttp:
|
||||||
else:
|
else:
|
||||||
client = httplib2.Http()
|
client = httplib2.Http()
|
||||||
|
|
||||||
_, content = client.request(url, "GET")
|
response, content = client.request(url, "GET")
|
||||||
|
|
||||||
self._logger.info("============================")
|
self._logger.info("============================")
|
||||||
self._logger.info("GET %s" % url)
|
self._logger.info("GET %s" % url)
|
||||||
|
@ -83,11 +94,10 @@ class OpenPhotoHttp:
|
||||||
|
|
||||||
self.last_url = url
|
self.last_url = url
|
||||||
self.last_params = params
|
self.last_params = params
|
||||||
self.last_response = content
|
self.last_response = (response, content)
|
||||||
|
|
||||||
if process_response:
|
if process_response:
|
||||||
return self._process_response(content)
|
return self._process_response(response, content)
|
||||||
return response
|
|
||||||
else:
|
else:
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
@ -95,11 +105,18 @@ class OpenPhotoHttp:
|
||||||
"""
|
"""
|
||||||
Performs an HTTP POST to the specified endpoint (API path),
|
Performs an HTTP POST to the specified endpoint (API path),
|
||||||
passing parameters if given.
|
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
|
Returns the decoded JSON dictionary, and raises exceptions if an
|
||||||
error code is received.
|
error code is received.
|
||||||
Returns the raw response if process_response=False
|
Returns the raw response if process_response=False
|
||||||
"""
|
"""
|
||||||
params = self._process_params(params)
|
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, '', '', ''))
|
url = urlparse.urlunparse(('http', self._host, endpoint, '', '', ''))
|
||||||
|
|
||||||
if not self._consumer_key:
|
if not self._consumer_key:
|
||||||
|
@ -111,28 +128,28 @@ class OpenPhotoHttp:
|
||||||
|
|
||||||
if files:
|
if files:
|
||||||
# Parameters must be signed and encoded into the multipart body
|
# Parameters must be signed and encoded into the multipart body
|
||||||
params = self._sign_params(client, url, params)
|
signed_params = self._sign_params(client, url, params)
|
||||||
headers, body = encode_multipart_formdata(params, files)
|
headers, body = encode_multipart_formdata(signed_params, files)
|
||||||
request = urllib2.Request(url, body, headers)
|
|
||||||
content = urllib2.urlopen(request).read()
|
|
||||||
else:
|
else:
|
||||||
body = urllib.urlencode(params)
|
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("============================")
|
||||||
self._logger.info("POST %s" % url)
|
self._logger.info("POST %s" % url)
|
||||||
if body:
|
self._logger.info("params: %s" % repr(params))
|
||||||
self._logger.info(body)
|
if files:
|
||||||
|
self._logger.info("files: %s" % repr(files))
|
||||||
self._logger.info("---")
|
self._logger.info("---")
|
||||||
self._logger.info(content)
|
self._logger.info(content)
|
||||||
|
|
||||||
self.last_url = url
|
self.last_url = url
|
||||||
self.last_params = params
|
self.last_params = params
|
||||||
self.last_response = content
|
self.last_response = (response, content)
|
||||||
|
|
||||||
if process_response:
|
if process_response:
|
||||||
return self._process_response(content)
|
return self._process_response(response, content)
|
||||||
else:
|
else:
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
@ -179,26 +196,32 @@ class OpenPhotoHttp:
|
||||||
return processed_params
|
return processed_params
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _process_response(content):
|
def _process_response(response, content):
|
||||||
"""
|
"""
|
||||||
Decodes the JSON response, returning a dict.
|
Decodes the JSON response, returning a dict.
|
||||||
Raises an exception if an invalid response code is received.
|
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:
|
if 200 <= code < 300:
|
||||||
# Valid response code
|
return json_response
|
||||||
return response
|
elif (code == DUPLICATE_RESPONSE["code"] and
|
||||||
|
DUPLICATE_RESPONSE["message"] in message):
|
||||||
error_message = "Code %d: %s" % (response["code"],
|
raise OpenPhotoDuplicateError("Code %d: %s" % (code, message))
|
||||||
response["message"])
|
else:
|
||||||
|
raise OpenPhotoError("Code %d: %s" % (code, 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)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _result_to_list(result):
|
def _result_to_list(result):
|
||||||
|
|
|
@ -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:
|
The easiest way to run a subset of the tests is with nose:
|
||||||
|
|
||||||
cd /path/to/openphoto-python
|
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>
|
<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
|
import test_base
|
||||||
|
|
||||||
class TestAlbums(test_base.TestBase):
|
class TestAlbums(test_base.TestBase):
|
||||||
|
testcase_name = "album API"
|
||||||
|
|
||||||
def test_create_delete(self):
|
def test_create_delete(self):
|
||||||
""" Create an album then delete it """
|
""" 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()])
|
self.assertIn(album_name, [a.name for a in self.client.albums.list()])
|
||||||
|
|
||||||
# Delete the album
|
# Delete the album
|
||||||
self.client.album.delete(album.id)
|
self.assertTrue(self.client.album.delete(album.id))
|
||||||
# Check that the album is now gone
|
# Check that the album is now gone
|
||||||
self.assertNotIn(album_name, [a.name for a in self.client.albums.list()])
|
self.assertNotIn(album_name, [a.name for a in self.client.albums.list()])
|
||||||
|
|
||||||
# Create it again, and delete it using the Album object
|
# Create it again, and delete it using the Album object
|
||||||
album = self.client.album.create(album_name)
|
album = self.client.album.create(album_name)
|
||||||
album.delete()
|
self.assertTrue(album.delete())
|
||||||
# Check that the album is now gone
|
# Check that the album is now gone
|
||||||
self.assertNotIn(album_name, [a.name for a in self.client.albums.list()])
|
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_TAG = "test_tag"
|
||||||
TEST_ALBUM = "test_album"
|
TEST_ALBUM = "test_album"
|
||||||
MAXIMUM_TEST_PHOTOS = 4 # Never have more the 4 photos on the test server
|
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):
|
def __init__(self, *args, **kwds):
|
||||||
unittest.TestCase.__init__(self, *args, **kwds)
|
unittest.TestCase.__init__(self, *args, **kwds)
|
||||||
self.photos = []
|
self.photos = []
|
||||||
|
|
||||||
LOG_FILENAME = "tests.log"
|
|
||||||
logging.basicConfig(filename="tests.log",
|
logging.basicConfig(filename="tests.log",
|
||||||
filemode="w",
|
filemode="w",
|
||||||
format="%(message)s",
|
format="%(message)s",
|
||||||
|
@ -22,8 +24,13 @@ class TestBase(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
""" Ensure there is nothing on the server before running any tests """
|
""" Ensure there is nothing on the server before running any tests """
|
||||||
config_file = os.getenv("OPENPHOTO_TEST_CONFIG", "test")
|
if cls.api_version is None:
|
||||||
cls.client = openphoto.OpenPhoto(config_file=config_file)
|
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() != []:
|
if cls.client.photos.list() != []:
|
||||||
raise ValueError("The test server (%s) contains photos. "
|
raise ValueError("The test server (%s) contains photos. "
|
||||||
|
@ -63,7 +70,7 @@ class TestBase(unittest.TestCase):
|
||||||
self.tags = self.client.tags.list()
|
self.tags = self.client.tags.list()
|
||||||
if (len(self.tags) != 1 or
|
if (len(self.tags) != 1 or
|
||||||
self.tags[0].id != self.TEST_TAG or
|
self.tags[0].id != self.TEST_TAG or
|
||||||
self.tags[0].count != 3):
|
str(self.tags[0].count) != "3"):
|
||||||
print "[Regenerating Tags]"
|
print "[Regenerating Tags]"
|
||||||
self._delete_all()
|
self._delete_all()
|
||||||
self._create_test_photos()
|
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
|
import test_base
|
||||||
|
|
||||||
class TestPhotos(test_base.TestBase):
|
class TestPhotos(test_base.TestBase):
|
||||||
|
testcase_name = "photo API"
|
||||||
|
|
||||||
def test_delete_upload(self):
|
def test_delete_upload(self):
|
||||||
""" Test photo deletion and upload """
|
""" Test photo deletion and upload """
|
||||||
# Delete one photo using the OpenPhoto class, passing in the id
|
# 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
|
# 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
|
# And another using the Photo object directly
|
||||||
self.photos[2].delete()
|
self.assertTrue(self.photos[2].delete())
|
||||||
|
|
||||||
# Check that they're gone
|
# Check that they're gone
|
||||||
self.assertEqual(self.client.photos.list(), [])
|
self.assertEqual(self.client.photos.list(), [])
|
||||||
|
@ -23,16 +25,18 @@ class TestPhotos(test_base.TestBase):
|
||||||
self.client.photo.upload_encoded("tests/test_photo3.jpg",
|
self.client.photo.upload_encoded("tests/test_photo3.jpg",
|
||||||
title=self.TEST_TITLE)
|
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.photos = self.client.photos.list()
|
||||||
self.assertEqual(len(self.photos), 3)
|
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
|
# Check that the upload return value was correct
|
||||||
pathOriginals = [photo.pathOriginal for photo in self.photos]
|
pathOriginals = [photo.pathOriginal for photo in self.photos]
|
||||||
self.assertIn(ret_val.pathOriginal, pathOriginals)
|
self.assertIn(ret_val.pathOriginal, pathOriginals)
|
||||||
|
|
||||||
# Delete all photos in one go
|
# Delete all photos in one go
|
||||||
self.client.photos.delete(self.photos)
|
self.assertTrue(self.client.photos.delete(self.photos))
|
||||||
|
|
||||||
# Check they're gone
|
# Check they're gone
|
||||||
self.photos = self.client.photos.list()
|
self.photos = self.client.photos.list()
|
||||||
|
@ -147,6 +151,12 @@ class TestPhotos(test_base.TestBase):
|
||||||
self.client.photo.dynamic_url(None)
|
self.client.photo.dynamic_url(None)
|
||||||
|
|
||||||
def test_transform(self):
|
def test_transform(self):
|
||||||
""" If photo.transform gets implemented, write a test! """
|
""" Test photo rotation """
|
||||||
with self.assertRaises(openphoto.NotImplementedError):
|
photo = self.photos[0]
|
||||||
self.client.photo.transform(None)
|
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
|
import test_base
|
||||||
|
|
||||||
class TestTags(test_base.TestBase):
|
class TestTags(test_base.TestBase):
|
||||||
@unittest.expectedFailure # Tag create fails - Issue #927
|
testcase_name = "tag API"
|
||||||
# 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 then delete it """
|
||||||
# Create a tag
|
# 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
|
# Create a tag on one of the photos
|
||||||
self.assertEqual(tag.id, tag_name)
|
self.photos[0].update(tagsAdd=tag_id)
|
||||||
# Check that the tag now exists
|
# 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
|
# Delete the tag
|
||||||
self.client.tag.delete(tag_name)
|
self.assertTrue(self.client.tag.delete(tag_id))
|
||||||
# Check that the tag is now gone
|
# 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
|
# Create then delete using the Tag object directly
|
||||||
tag = self.client.tag.create(tag_name)
|
self.photos[0].update(tagsAdd=tag_id)
|
||||||
tag.delete()
|
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
|
# 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
|
# TODO: Un-skip and update this tests once there are tag fields that can be updated.
|
||||||
# NOTE: the below has not been tested/debugged, since it fails at the first step
|
# 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):
|
def test_update(self):
|
||||||
""" Test that a tag can be updated """
|
""" Test that a tag can be updated """
|
||||||
# Update the tag using the OpenPhoto class, passing in the tag object
|
# 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(self.tags[0].owner, owner)
|
||||||
self.assertEqual(ret_val.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):
|
def test_tag_with_spaces(self):
|
||||||
""" Run test_create_delete using a tag containing spaces """
|
""" Run test_create_delete using a tag containing spaces """
|
||||||
self.test_create_delete("tag with spaces")
|
self.test_create_delete("tag with spaces")
|
||||||
|
|
||||||
# We mustn't run this test until Issue #919 is resolved,
|
def test_tag_with_slashes(self):
|
||||||
# since it creates an undeletable tag
|
|
||||||
@unittest.skip("Tags with double-slashes cannot be deleted - Issue #919")
|
|
||||||
def test_tag_with_double_slashes(self):
|
|
||||||
""" Run test_create_delete using a tag containing slashes """
|
""" 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