Merge pull request #29 from sneakypete81/api_versioning
API versioning support
This commit is contained in:
commit
e3bc3ea9f7
12 changed files with 143 additions and 16 deletions
|
@ -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
|
||||
|
||||
<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(host, consumerKey, consumerSecret, token, tokenSecret, api_version=2)
|
||||
|
||||
----------------------------------------
|
||||
|
||||
<a name="cli"></a>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -88,13 +88,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):
|
||||
|
|
|
@ -18,12 +18,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")
|
||||
|
||||
|
@ -36,11 +37,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:
|
||||
|
@ -70,13 +78,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")
|
||||
|
||||
|
|
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 """
|
||||
|
|
|
@ -21,6 +21,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)
|
||||
|
@ -34,9 +36,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. "
|
||||
|
@ -67,7 +75,7 @@ class TestBase(unittest.TestCase):
|
|||
self.photos = self.client.photos.list()
|
||||
if len(self.photos) != 3:
|
||||
# print self.photos
|
||||
print "\n[Regenerating Photos]"
|
||||
print "[Regenerating Photos]"
|
||||
if len(self.photos) > 0:
|
||||
self._delete_all()
|
||||
self._create_test_photos()
|
||||
|
@ -76,8 +84,8 @@ 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):
|
||||
print "\n[Regenerating Tags]"
|
||||
str(self.tags[0].count) != "3"):
|
||||
print "[Regenerating Tags]"
|
||||
self._delete_all()
|
||||
self._create_test_photos()
|
||||
self.photos = self.client.photos.list()
|
||||
|
@ -90,7 +98,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 "\n[Regenerating Albums]"
|
||||
print "[Regenerating Albums]"
|
||||
self._delete_all()
|
||||
self._create_test_photos()
|
||||
self.photos = self.client.photos.list()
|
||||
|
|
48
tests/test_framework.py
Normal file
48
tests/test_framework.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
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 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(openphoto.OpenPhoto404Error):
|
||||
client.get("hello.json")
|
|
@ -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
|
||||
|
|
|
@ -3,6 +3,8 @@ import openphoto
|
|||
import test_base
|
||||
|
||||
class TestTags(test_base.TestBase):
|
||||
testcase_name = "tag API"
|
||||
|
||||
def test_create_delete(self, tag_id="create_tag"):
|
||||
""" Create a tag then delete it """
|
||||
# Create a tag
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue