Extended API to add pythonic classes/methods. See the updated README.markdown for examples

This commit is contained in:
sneakypete81 2012-08-28 18:59:33 +01:00
parent ccc6e6ba1c
commit b418cf7e78
9 changed files with 457 additions and 60 deletions

View file

@ -14,10 +14,26 @@ python setup.py install
To use the library you need to first ``import openphoto``, then instantiate an instance of the class and start making calls.
You can use the library in one of two ways:
* Direct GET/POST calls to the server
* Access via Python classes/methods
<a name="get_post"></a>
### Direct GET/POST:
from openphoto import OpenPhoto
client = OpenPhoto(host, consumerKey, consumerSecret, token, tokenSecret)
resp = client.get('/photos/list.json')
resp = client.post('/photo/62/update.json', {'tags': 'tag1,tag2'})
resp = client.get("/photos/list.json")
resp = client.post("/photo/62/update.json", tags=["tag1", "tag2"])
<a name="python_classes"></a>
### Python classes/methods
from openphoto import OpenPhoto
client = OpenPhoto(host, consumerKey, consumerSecret, token, tokenSecret)
photos = client.photos_list()
photos[0].update(tags=["tag1", "tag2"])
print photos[0].tags
----------------------------------------

View file

@ -1,50 +1,8 @@
import oauth2 as oauth
import urlparse
import urllib
import httplib2
import types
from openphoto_http import OpenPhotoHttp, OpenPhotoError, OpenPhotoDuplicateError
from api_photo import ApiPhoto
from api_tag import ApiTag
from api_album import ApiAlbum
class OpenPhoto(object):
class OpenPhoto(OpenPhotoHttp, ApiPhoto, ApiTag, ApiAlbum):
""" Client library for OpenPhoto """
def __init__(self, host, consumer_key='', consumer_secret='',
token='', token_secret=''):
self.host = host
self.consumer_key = consumer_key
self.consumer_secret = consumer_secret
self.token = token
self.token_secret = token_secret
def get(self, endpoint, params={}):
url = urlparse.urlunparse(('http', self.host, endpoint, '',
urllib.urlencode(params), ''))
if self.consumer_key:
consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
token = oauth.Token(self.token, self.token_secret)
client = oauth.Client(consumer, token)
else:
client = httplib2.Http()
headers, content = client.request(url, "GET")
return content
def post(self, endpoint, params={}):
url = urlparse.urlunparse(('http', self.host, endpoint, '', '', ''))
if self.consumer_key:
consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
token = oauth.Token(self.token, self.token_secret)
# ensure utf-8 encoding for all values.
params = dict([(k, v.encode('utf-8')
if type(v) is types.UnicodeType else v)
for (k, v) in params.items()])
client = oauth.Client(consumer, token)
body = urllib.urlencode(params)
headers, content = client.request(url, "POST", body)
return content
pass

33
openphoto/api_album.py Normal file
View file

@ -0,0 +1,33 @@
from openphoto_http import OpenPhotoHttp, OpenPhotoError
from objects import Album
class ApiAlbum(OpenPhotoHttp):
def album_create(self, name, **kwds):
""" Create a new album and return it"""
result = self.post("/album/create.json", name=name, **kwds)["result"]
return Album(self, result)
def album_delete(self, album_id, **kwds):
""" Delete an album """
album = Album(self, {"id": album_id})
album.delete(**kwds)
def album_form(self, album_id, **kwds):
raise NotImplementedError()
def album_add_photos(self, album_id, photo_ids, **kwds):
raise NotImplementedError()
def album_remove_photos(self, album_id, photo_ids, **kwds):
raise NotImplementedError()
def albums_list(self, **kwds):
""" Return a list of Album objects """
results = self.get("/albums/list.json", **kwds)["result"]
return [Album(self, album) for album in results]
def album_update(self, album_id, **kwds):
""" Update an album """
album = Album(self, {"id": album_id})
album.update(**kwds)
# Don't return the album, since the API doesn't give us the modified album

81
openphoto/api_photo.py Normal file
View file

@ -0,0 +1,81 @@
import base64
from openphoto_http import OpenPhotoHttp, OpenPhotoError
from objects import Photo
class ApiPhoto(OpenPhotoHttp):
def photo_delete(self, photo_id, **kwds):
""" Delete a photo """
photo = Photo(self, {"id": photo_id})
photo.delete(**kwds)
def photo_edit(self, photo_id, **kwds):
""" Returns an HTML form to edit a photo """
photo = Photo(self, {"id": photo_id})
return photo.edit(**kwds)
def photo_replace(self, photo_id, photo_file, **kwds):
raise NotImplementedError()
def photo_replace_encoded(self, photo_id, photo_file, **kwds):
raise NotImplementedError()
def photo_update(self, photo_id, **kwds):
"""
Update a photo with the specified parameters.
Returns the updated photo object
"""
photo = Photo(self, {"id": photo_id})
photo.update(**kwds)
return photo
def photo_view(self, photo_id, **kwds):
"""
Used to view the photo at a particular size.
Returns the requested photo object
"""
photo = Photo(self, {"id": photo_id})
photo.view(**kwds)
return photo
def photos_list(self, **kwds):
""" Returns a list of Photo objects """
photos = self.get("/photos/list.json", **kwds)["result"]
photos = self._result_to_list(photos)
return [Photo(self, photo) for photo in photos]
def photos_update(self, photo_ids, **kwds):
""" Updates a list of photos """
if not self._openphoto.post("/photos/update.json" % photo_ids,
**kwds)["result"]:
raise OpenPhotoError("Update response returned False")
def photos_delete(self, photo_ids, **kwds):
""" Deletes a list of photos """
if not self._openphoto.post("/photos/delete.json" % photo_ids,
**kwds)["result"]:
raise OpenPhotoError("Delete response returned False")
def photo_upload(self, photo_file, **kwds):
raise NotImplementedError("Use photo_upload_encoded instead.")
def photo_upload_encoded(self, photo_file, **kwds):
""" Base64-encodes and uploads the specified file """
encoded_photo = base64.b64encode(open(photo_file, "rb").read())
result = self.post("/photo/upload.json", photo=encoded_photo,
**kwds)["result"]
return Photo(self, result)
def photo_dynamic_url(self, photo_id, **kwds):
raise NotImplementedError()
def photo_next_previous(self, photo_id, **kwds):
"""
Returns a dict containing the next and previous photo objects,
given a photo in the middle.
"""
photo = Photo(self, {"id": photo_id})
return photo.next_previous(**kwds)
def photo_transform(self, photo_id, **kwds):
raise NotImplementedError()

25
openphoto/api_tag.py Normal file
View file

@ -0,0 +1,25 @@
from openphoto_http import OpenPhotoHttp, OpenPhotoError
from objects import Tag
class ApiTag(OpenPhotoHttp):
def tag_create(self, tag_id, **kwds):
""" Create a new tag and return it """
result = self.post("/tag/create.json", tag=tag_id, **kwds)["result"]
return Tag(self, result)
def tag_delete(self, tag_id, **kwds):
""" Delete a tag """
tag = Tag(self, {"id": tag_id})
tag.delete(**kwds)
def tag_update(self, tag_id, **kwds):
""" Update a tag """
tag = Tag(self, {"id": tag_id})
tag.update(**kwds)
return tag
def tags_list(self, **kwds):
""" Returns a list of Tag objects """
results = self.get("/tags/list.json", **kwds)["result"]
return [Tag(self, tag) for tag in results]

View file

@ -6,9 +6,9 @@ import urllib
from optparse import OptionParser
try:
import simplejson as json
except:
import json
except ImportError:
import simplejson as json
from openphoto import OpenPhoto
@ -45,9 +45,9 @@ def main(args=sys.argv[1:]):
client = OpenPhoto(options.host, consumer_key, consumer_secret, token, token_secret)
if options.method == "GET":
result = client.get(options.endpoint, params)
result = client.get_raw(options.endpoint, **params)
else:
result = client.post(options.endpoint, params)
result = client.post_raw(options.endpoint, **params)
if options.verbose:
print "==========\nMethod: %s\nHost: %s\nEndpoint: %s" % (options.method, options.host, options.endpoint)

147
openphoto/objects.py Normal file
View file

@ -0,0 +1,147 @@
from openphoto_http import OpenPhotoError, NotImplementedError
class OpenPhotoObject:
""" Base object supporting the storage of custom fields as attributes """
def __init__(self, openphoto, json_dict):
self._openphoto = openphoto
self._json_dict = json_dict
self._set_fields(json_dict)
def _set_fields(self, json_dict):
""" Set this object's attributes specified in json_dict """
for key, value in json_dict.items():
if key.startswith("_"):
raise ValueError("Illegal attribute: %s" % key)
setattr(self, key, value)
def _replace_fields(self, json_dict):
"""
Delete this object's attributes, and replace with
those in json_dict.
"""
for key in self._json_dict.keys():
delattr(self, key)
self._json_dict = json_dict
self._set_fields(json_dict)
def __repr__(self):
if hasattr(self, "name"):
return "<%s name='%s'>" % (self.__class__, self.name)
elif hasattr(self, "id"):
return "<%s id='%s'>" % (self.__class__, self.id)
else:
return "<%s>" % (self.__class__)
def get_fields(self):
""" Returns this object's attributes """
return self._json_dict
class Photo(OpenPhotoObject):
def delete(self, **kwds):
""" Delete this photo """
self._openphoto.post("/photo/%s/delete.json" % self.id, **kwds)
self._replace_fields({})
def edit(self, **kwds):
""" Returns an HTML form to edit the photo """
result = self._openphoto.get("/photo/%s/edit.json" % self.id,
**kwds)["result"]
return result["markup"]
def replace(self, photo_file, **kwds):
raise NotImplementedError()
def replace_encoded(self, encoded_photo, **kwds):
raise NotImplementedError()
def update(self, **kwds):
""" Update this photo with the specified parameters """
new_dict = self._openphoto.post("/photo/%s/update.json" % self.id,
**kwds)["result"]
self._replace_fields(new_dict)
def view(self, **kwds):
"""
Used to view the photo at a particular size.
Updates the photo's fields with the response.
"""
new_dict = self._openphoto.get("/photo/%s/view.json" % self.id,
**kwds)["result"]
self._replace_fields(new_dict)
def dynamic_url(self, **kwds):
raise NotImplementedError()
def next_previous(self, **kwds):
""" Returns a dict containing the next and previous photo objects """
result = self._openphoto.get("/photo/%s/nextprevious.json" % self.id,
**kwds)["result"]
value = {}
if "next" in result:
value["next"] = Photo(self._openphoto, result["next"])
if "previous" in result:
value["previous"] = Photo(self._openphoto, result["previous"])
return value
def transform(self, **kwds):
raise NotImplementedError()
class Tag(OpenPhotoObject):
def delete(self, **kwds):
""" Delete this tag """
self._openphoto.post("/tag/%s/delete.json" % 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,
**kwds)["result"]
self._replace_fields(new_dict)
class Album(OpenPhotoObject):
def __init__(self, openphoto, json_dict):
OpenPhotoObject.__init__(self, openphoto, json_dict)
# Update the cover attribute with a photo object
if hasattr(self, "cover") and self.cover is not None:
self.cover = Photo(openphoto, self.cover)
def delete(self, **kwds):
""" Delete this album """
self._openphoto.post("/album/%s/delete.json" % self.id, **kwds)
self._replace_fields({})
def form(self, **kwds):
raise NotImplementedError()
def add_photos(self, **kwds):
raise NotImplementedError()
def remove_photos(self, **kwds):
raise NotImplementedError()
def update(self, **kwds):
""" Update this album with the specified parameters """
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)
def view(self, **kwds):
"""
Requests the full contents of the album.
Updates the album's fields with the response.
"""
result = self._openphoto.get("/album/%s/view.json" % self.id,
**kwds)["result"]
# Update the cover attribute with a photo object
if result["cover"] is not None:
result["cover"] = Photo(self._openphoto, result["cover"])
# Update the photo list with photo objects
for i, photo in enumerate(result["photos"]):
result["photos"][i] = Photo(self._openphoto, result["photos"][i])
self._replace_fields(result)

135
openphoto/openphoto_http.py Normal file
View file

@ -0,0 +1,135 @@
import oauth2 as oauth
import urlparse
import urllib
import httplib2
try:
import json
except ImportError:
import simplejson as json
class OpenPhotoError(Exception):
""" Indicates that an OpenPhoto operation failed """
pass
class OpenPhotoDuplicateError(OpenPhotoError):
""" Indicates that an upload operation failed due to a duplicate photo """
pass
class NotImplementedError(OpenPhotoError):
""" Indicates that the API function has not yet been coded - please help! """
pass
DUPLICATE_RESPONSE = {"code": 409,
"message": "This photo already exists"}
class OpenPhotoHttp:
""" Base class to handle HTTP requests to an OpenPhoto server """
def __init__(self, host, consumer_key='', consumer_secret='',
token='', token_secret=''):
self._host = host
self._consumer_key = consumer_key
self._consumer_secret = consumer_secret
self._token = token
self._token_secret = token_secret
def get(self, endpoint, **params):
"""
Performs an HTTP GET from the specified endpoint (API path),
passing parameters if given.
Returns the decoded JSON dictionary, and
raises exceptions if an error code is received.
"""
response = json.loads(self.get_raw(endpoint, **params))
self._process_response(response)
return response
def post(self, endpoint, **params):
"""
Performs an HTTP POST to the specified endpoint (API path),
passing parameters if given.
Returns the decoded JSON dictionary, and
raises exceptions if an error code is received.
"""
response = json.loads(self.post_raw(endpoint, **params))
self._process_response(response)
return response
def get_raw(self, endpoint, **params):
"""
Performs an HTTP GET from the specified endpoint (API path),
passing parameters if given.
Returns the raw HTTP content string.
"""
params = self._process_params(params)
url = urlparse.urlunparse(('http', self._host, endpoint, '',
urllib.urlencode(params), ''))
if self._consumer_key:
consumer = oauth.Consumer(self._consumer_key, self._consumer_secret)
token = oauth.Token(self._token, self._token_secret)
client = oauth.Client(consumer, token)
else:
client = httplib2.Http()
_, content = client.request(url, "GET")
return content
def post_raw(self, endpoint, **params):
"""
Performs an HTTP POST to the specified endpoint (API path),
passing parameters if given.
Returns the raw HTTP content string.
"""
params = self._process_params(params)
url = urlparse.urlunparse(('http', self._host, endpoint, '', '', ''))
if not self._consumer_key:
raise OpenPhotoError("Cannot issue POST without OAuth tokens")
consumer = oauth.Consumer(self._consumer_key, self._consumer_secret)
token = oauth.Token(self._token, self._token_secret)
client = oauth.Client(consumer, token)
body = urllib.urlencode(params)
_, content = client.request(url, "POST", body)
return content
@staticmethod
def _process_params(params):
""" Converts Unicode/lists/booleans inside HTTP parameters """
processed_params = {}
for key, value in params.items():
# Use UTF-8 encoding
if isinstance(value, unicode):
value = value.encode('utf-8')
# Handle lists
if isinstance(value, list):
value = ",".join(value)
# Handle booleans
if isinstance(value, bool):
value = 1 if value else 0
processed_params[key] = value
return processed_params
@staticmethod
def _process_response(response):
""" Raises an exception if an invalid response code is received """
if response["code"] >= 200 and response["code"] < 300:
return
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)
@staticmethod
def _result_to_list(result):
""" Handle the case where the result contains no items """
if result[0]["totalRows"] == 0:
return []
else:
return result

4
setup.py Normal file → Executable file
View file

@ -1,3 +1,5 @@
#!/usr/bin/env python
requires = ['oauth2', 'httplib2']
try:
import json
@ -17,7 +19,7 @@ except ImportError:
'requires': requires}
setup(name='openphoto',
version='0.1',
version='0.2',
description='Client library for the openphoto project',
author='James Walker',
author_email='walkah@walkah.net',