Python3 support

This commit is contained in:
sneakypete81 2013-05-12 20:54:57 +01:00
parent 6c75abc9a8
commit 0805f032fb
14 changed files with 131 additions and 96 deletions

View file

@ -1,8 +1,8 @@
from openphoto_http import OpenPhotoHttp from .openphoto_http import OpenPhotoHttp
from errors import * from .errors import *
import api_photo from . import api_photo
import api_tag from . import api_tag
import api_album from . import api_album
LATEST_API_VERSION = 2 LATEST_API_VERSION = 2

View file

@ -1,5 +1,5 @@
from errors import * from .errors import *
from objects import Album from .objects import Album
class ApiAlbums: class ApiAlbums:
def __init__(self, client): def __init__(self, client):

View file

@ -1,7 +1,7 @@
import base64 import base64
from errors import * from .errors import *
from objects import Photo from .objects import Photo
class ApiPhotos: class ApiPhotos:
def __init__(self, client): def __init__(self, client):
@ -80,14 +80,16 @@ class ApiPhoto:
return photo return photo
def upload(self, photo_file, **kwds): def upload(self, photo_file, **kwds):
with open(photo_file, 'rb') as f:
result = self._client.post("/photo/upload.json", result = self._client.post("/photo/upload.json",
files={'photo': open(photo_file, 'rb')}, files={'photo': f},
**kwds)["result"] **kwds)["result"]
return Photo(self._client, result) return Photo(self._client, result)
def upload_encoded(self, photo_file, **kwds): def upload_encoded(self, photo_file, **kwds):
""" Base64-encodes and uploads the specified file """ """ Base64-encodes and uploads the specified file """
encoded_photo = base64.b64encode(open(photo_file, "rb").read()) with open(photo_file, "rb") as f:
encoded_photo = base64.b64encode(f.read())
result = self._client.post("/photo/upload.json", photo=encoded_photo, result = self._client.post("/photo/upload.json", photo=encoded_photo,
**kwds)["result"] **kwds)["result"]
return Photo(self._client, result) return Photo(self._client, result)

View file

@ -1,5 +1,5 @@
from errors import * from .errors import *
from objects import Tag from .objects import Tag
class ApiTags: class ApiTags:
def __init__(self, client): def __init__(self, client):

View file

@ -2,7 +2,6 @@
import os import os
import sys import sys
import string import string
import urllib
from optparse import OptionParser from optparse import OptionParser
try: try:
@ -56,22 +55,22 @@ def main(args=sys.argv[1:]):
try: try:
client = OpenPhoto(config_file=options.config_file) client = OpenPhoto(config_file=options.config_file)
except IOError as error: except IOError as error:
print error print(error)
print print()
print "You must create a configuration file with the following contents:" print("You must create a configuration file with the following contents:")
print " host = your.host.com" print(" host = your.host.com")
print " consumerKey = your_consumer_key" print(" consumerKey = your_consumer_key")
print " consumerSecret = your_consumer_secret" print(" consumerSecret = your_consumer_secret")
print " token = your_access_token" print(" token = your_access_token")
print " tokenSecret = your_access_token_secret" print(" tokenSecret = your_access_token_secret")
print print()
print "To get your credentials:" print("To get your credentials:")
print " * Log into your Trovebox site" print(" * Log into your Trovebox site")
print " * Click the arrow on the top-right and select 'Settings'." print(" * Click the arrow on the top-right and select 'Settings'.")
print " * Click the 'Create a new app' button." print(" * Click the 'Create a new app' button.")
print " * Click the 'View' link beside the newly created app." print(" * Click the 'View' link beside the newly created app.")
print print()
print error print(error)
sys.exit(1) sys.exit(1)
if options.method == "GET": if options.method == "GET":
@ -81,17 +80,17 @@ def main(args=sys.argv[1:]):
result = client.post(options.endpoint, process_response=False, files=files, **params) result = client.post(options.endpoint, process_response=False, files=files, **params)
if options.verbose: if options.verbose:
print "==========\nMethod: %s\nHost: %s\nEndpoint: %s" % (options.method, config['host'], options.endpoint) print("==========\nMethod: %s\nHost: %s\nEndpoint: %s" % (options.method, config['host'], options.endpoint))
if len( params ) > 0: if len( params ) > 0:
print "Fields:" print("Fields:")
for kv in params.iteritems(): for kv in params.items():
print " %s=%s" % kv print(" %s=%s" % kv)
print "==========\n" print("==========\n")
if options.pretty: if options.pretty:
print json.dumps(json.loads(result), sort_keys=True, indent=4, separators=(',',':')) print(json.dumps(json.loads(result), sort_keys=True, indent=4, separators=(',',':')))
else: else:
print result print(result)
def extract_files(params): def extract_files(params):
""" """

View file

@ -1,5 +1,8 @@
import urllib try:
from errors import * from urllib.parse import quote # Python3
except ImportError:
from urllib import quote # Python2
from .errors import *
class OpenPhotoObject: class OpenPhotoObject:
""" Base object supporting the storage of custom fields as attributes """ """ Base object supporting the storage of custom fields as attributes """
@ -128,13 +131,13 @@ class Tag(OpenPhotoObject):
Returns True if successful. Returns True if successful.
Raises an OpenPhotoError if not. Raises an OpenPhotoError if not.
""" """
result = self._openphoto.post("/tag/%s/delete.json" % urllib.quote(self.id), **kwds)["result"] result = self._openphoto.post("/tag/%s/delete.json" % quote(self.id), **kwds)["result"]
self._replace_fields({}) self._replace_fields({})
return result 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" % urllib.quote(self.id), new_dict = self._openphoto.post("/tag/%s/update.json" % quote(self.id),
**kwds)["result"] **kwds)["result"]
self._replace_fields(new_dict) self._replace_fields(new_dict)

View file

@ -1,18 +1,36 @@
from __future__ import unicode_literals
import sys
import os import os
import urlparse try:
import urllib from urllib.parse import urlunparse # Python3
except ImportError:
from urlparse import urlunparse # Python2
import requests import requests
import requests_oauthlib import requests_oauthlib
import logging import logging
import StringIO
import ConfigParser
try: try:
import json import io # Python3
except ImportError: except ImportError:
import simplejson as json import StringIO as io # Python2
try:
from configparser import ConfigParser # Python3
except ImportError:
from ConfigParser import SafeConfigParser as ConfigParser # Python2
from objects import OpenPhotoObject if sys.version < '3':
from errors import * text_type = unicode # Python2
else:
text_type = str # Python3
from .objects import OpenPhotoObject
from .errors import *
if sys.version < '3':
# requests_oauth needs to decode to ascii for Python2
_oauth_decoding = "utf-8"
else:
# requests_oauth needs to use (unicode) strings for Python3
_oauth_decoding = None # Python3
DUPLICATE_RESPONSE = {"code": 409, DUPLICATE_RESPONSE = {"code": 409,
"message": "This photo already exists"} "message": "This photo already exists"}
@ -75,15 +93,17 @@ class OpenPhotoHttp:
endpoint = "/" + endpoint endpoint = "/" + endpoint
if self._api_version is not None: if self._api_version is not None:
endpoint = "/v%d%s" % (self._api_version, endpoint) endpoint = "/v%d%s" % (self._api_version, endpoint)
url = urlparse.urlunparse(('http', self._host, endpoint, '', '', '')) url = urlunparse(('http', self._host, endpoint, '', '', ''))
if self._consumer_key: if self._consumer_key:
auth = requests_oauthlib.OAuth1(self._consumer_key, self._consumer_secret, auth = requests_oauthlib.OAuth1(self._consumer_key, self._consumer_secret,
self._token, self._token_secret) self._token, self._token_secret,
decoding=_oauth_decoding)
else: else:
auth = None auth = None
response = requests.get(url, params=params, auth=auth) with requests.Session() as s:
response = s.get(url, params=params, auth=auth)
self._logger.info("============================") self._logger.info("============================")
self._logger.info("GET %s" % url) self._logger.info("GET %s" % url)
@ -115,20 +135,22 @@ class OpenPhotoHttp:
endpoint = "/" + endpoint endpoint = "/" + endpoint
if self._api_version is not None: if self._api_version is not None:
endpoint = "/v%d%s" % (self._api_version, endpoint) endpoint = "/v%d%s" % (self._api_version, endpoint)
url = urlparse.urlunparse(('http', self._host, endpoint, '', '', '')) url = urlunparse(('http', self._host, endpoint, '', '', ''))
if not self._consumer_key: if not self._consumer_key:
raise OpenPhotoError("Cannot issue POST without OAuth tokens") raise OpenPhotoError("Cannot issue POST without OAuth tokens")
auth = requests_oauthlib.OAuth1(self._consumer_key, self._consumer_secret, auth = requests_oauthlib.OAuth1(self._consumer_key, self._consumer_secret,
self._token, self._token_secret) self._token, self._token_secret,
decoding=_oauth_decoding)
with requests.Session() as s:
if files: if files:
# Need to pass parameters as URL query, so they get OAuth signed # Need to pass parameters as URL query, so they get OAuth signed
response = requests.post(url, params=params, files=files, auth=auth) response = s.post(url, params=params, files=files, auth=auth)
else: else:
# Passing parameters as URL query doesn't work if there are no files to send. # Passing parameters as URL query doesn't work if there are no files to send.
# Send them as form data instead. # Send them as form data instead.
response = requests.post(url, data=params, auth=auth) response = s.post(url, data=params, auth=auth)
self._logger.info("============================") self._logger.info("============================")
self._logger.info("POST %s" % url) self._logger.info("POST %s" % url)
@ -156,9 +178,9 @@ class OpenPhotoHttp:
if isinstance(value, OpenPhotoObject): if isinstance(value, OpenPhotoObject):
value = value.id value = value.id
# Use UTF-8 encoding # Ensure value is UTF-8 encoded
if isinstance(value, unicode): if isinstance(value, text_type):
value = value.encode('utf-8') value = value.encode("utf-8")
# Handle lists # Handle lists
if isinstance(value, list): if isinstance(value, list):
@ -168,8 +190,8 @@ class OpenPhotoHttp:
for i, item in enumerate(new_list): for i, item in enumerate(new_list):
if isinstance(item, OpenPhotoObject): if isinstance(item, OpenPhotoObject):
new_list[i] = item.id new_list[i] = item.id
# Convert list to unicode string # Convert list to string
value = u','.join([unicode(item) for item in new_list]) value = ','.join([str(item) for item in new_list])
# Handle booleans # Handle booleans
if isinstance(value, bool): if isinstance(value, bool):
@ -188,7 +210,7 @@ class OpenPhotoHttp:
json_response = response.json() json_response = response.json()
code = json_response["code"] code = json_response["code"]
message = json_response["message"] message = json_response["message"]
except ValueError, KeyError: except (ValueError, KeyError):
# Response wasn't OpenPhoto JSON - check the HTTP status code # Response wasn't OpenPhoto JSON - check the HTTP status code
if 200 <= response.status_code < 300: if 200 <= response.status_code < 300:
# Status code was valid, so just reraise the exception # Status code was valid, so just reraise the exception
@ -236,14 +258,18 @@ class OpenPhotoHttp:
'token': '', 'tokenSecret':'', 'token': '', 'tokenSecret':'',
} }
# Insert an section header at the start of the config file, so ConfigParser can understand it # Insert an section header at the start of the config file, so ConfigParser can understand it
buf = StringIO.StringIO() buf = io.StringIO()
buf.write('[%s]\n' % section) buf.write('[%s]\n' % section)
buf.write(open(config_file).read()) with io.open(config_file, "r") as f:
buf.write(f.read())
buf.seek(0, os.SEEK_SET) buf.seek(0, os.SEEK_SET)
parser = ConfigParser.SafeConfigParser() parser = ConfigParser()
parser.optionxform = str # Case-sensitive options parser.optionxform = str # Case-sensitive options
parser.readfp(buf) try:
parser.read_file(buf) # Python3
except AttributeError:
parser.readfp(buf) # Python2
# Trim quotes # Trim quotes
config = parser.items(section) config = parser.items(section)

View file

@ -3,7 +3,7 @@ try:
except ImportError: except ImportError:
import unittest import unittest
import openphoto import openphoto
import test_base from . import test_base
class TestAlbums(test_base.TestBase): class TestAlbums(test_base.TestBase):
testcase_name = "album API" testcase_name = "album API"

View file

@ -1,3 +1,4 @@
from __future__ import print_function
import sys import sys
import os import os
try: try:
@ -35,9 +36,9 @@ class TestBase(unittest.TestCase):
""" Ensure there is nothing on the server before running any tests """ """ Ensure there is nothing on the server before running any tests """
if cls.debug: if cls.debug:
if cls.api_version is None: if cls.api_version is None:
print "\nTesting Latest %s" % cls.testcase_name print("\nTesting Latest %s" % cls.testcase_name)
else: else:
print "\nTesting %s v%d" % (cls.testcase_name, cls.api_version) print("\nTesting %s v%d" % (cls.testcase_name, cls.api_version))
cls.client = openphoto.OpenPhoto(config_file=cls.config_file, cls.client = openphoto.OpenPhoto(config_file=cls.config_file,
api_version=cls.api_version) api_version=cls.api_version)
@ -71,9 +72,9 @@ class TestBase(unittest.TestCase):
self.photos = self.client.photos.list() self.photos = self.client.photos.list()
if len(self.photos) != 3: if len(self.photos) != 3:
if self.debug: if self.debug:
print "[Regenerating Photos]" print("[Regenerating Photos]")
else: else:
print " ", print(" ", end='')
sys.stdout.flush() sys.stdout.flush()
if len(self.photos) > 0: if len(self.photos) > 0:
self._delete_all() self._delete_all()
@ -85,16 +86,16 @@ class TestBase(unittest.TestCase):
self.tags[0].id != self.TEST_TAG or self.tags[0].id != self.TEST_TAG or
str(self.tags[0].count) != "3"): str(self.tags[0].count) != "3"):
if self.debug: if self.debug:
print "[Regenerating Tags]" print("[Regenerating Tags]")
else: else:
print " ", print(" ", end='')
sys.stdout.flush() sys.stdout.flush()
self._delete_all() self._delete_all()
self._create_test_photos() self._create_test_photos()
self.photos = self.client.photos.list() self.photos = self.client.photos.list()
self.tags = self.client.tags.list() self.tags = self.client.tags.list()
if len(self.tags) != 1: if len(self.tags) != 1:
print "Tags: %s" % self.tags print("Tags: %s" % self.tags)
raise Exception("Tag creation failed") raise Exception("Tag creation failed")
self.albums = self.client.albums.list() self.albums = self.client.albums.list()
@ -102,9 +103,9 @@ class TestBase(unittest.TestCase):
self.albums[0].name != self.TEST_ALBUM or self.albums[0].name != self.TEST_ALBUM or
self.albums[0].count != "3"): self.albums[0].count != "3"):
if self.debug: if self.debug:
print "[Regenerating Albums]" print("[Regenerating Albums]")
else: else:
print " ", print(" ", end='')
sys.stdout.flush() sys.stdout.flush()
self._delete_all() self._delete_all()
self._create_test_photos() self._create_test_photos()
@ -112,7 +113,7 @@ class TestBase(unittest.TestCase):
self.tags = self.client.tags.list() self.tags = self.client.tags.list()
self.albums = self.client.albums.list() self.albums = self.client.albums.list()
if len(self.albums) != 1: if len(self.albums) != 1:
print "Albums: %s" % self.albums print("Albums: %s" % self.albums)
raise Exception("Album creation failed") raise Exception("Album creation failed")
logging.info("\nRunning %s..." % self.id()) logging.info("\nRunning %s..." % self.id())

View file

@ -27,7 +27,7 @@ class TestConfig(unittest.TestCase):
shutil.rmtree(CONFIG_HOME_PATH, ignore_errors=True) shutil.rmtree(CONFIG_HOME_PATH, ignore_errors=True)
def create_config(self, config_file, host): def create_config(self, config_file, host):
f = open(os.path.join(CONFIG_PATH, config_file), "w") with open(os.path.join(CONFIG_PATH, config_file), "w") as f:
f.write("host = %s\n" % host) f.write("host = %s\n" % host)
f.write("# Comment\n\n") f.write("# Comment\n\n")
f.write("consumerKey = \"%s_consumer_key\"\n" % config_file) f.write("consumerKey = \"%s_consumer_key\"\n" % config_file)

View file

@ -4,7 +4,7 @@ except ImportError:
import unittest import unittest
import logging import logging
import openphoto import openphoto
import test_base from . import test_base
class TestFramework(test_base.TestBase): class TestFramework(test_base.TestBase):
testcase_name = "framework" testcase_name = "framework"

View file

@ -1,9 +1,10 @@
from __future__ import unicode_literals
try: try:
import unittest2 as unittest import unittest2 as unittest
except ImportError: except ImportError:
import unittest import unittest
import openphoto import openphoto
import test_base from . import test_base
class TestPhotos(test_base.TestBase): class TestPhotos(test_base.TestBase):
testcase_name = "photo API" testcase_name = "photo API"
@ -72,7 +73,7 @@ class TestPhotos(test_base.TestBase):
def test_update(self): def test_update(self):
""" Update a photo by editing the title """ """ Update a photo by editing the title """
title = u"\xfcmlaut" # umlauted umlaut title = "\xfcmlaut" # umlauted umlaut
# Get a photo and check that it doesn't have the magic title # Get a photo and check that it doesn't have the magic title
photo = self.photos[0] photo = self.photos[0]
self.assertNotEqual(photo.title, title) self.assertNotEqual(photo.title, title)

View file

@ -3,7 +3,7 @@ try:
except ImportError: except ImportError:
import unittest import unittest
import openphoto import openphoto
import test_base from . import test_base
@unittest.skipIf(test_base.get_test_server_api() == 1, @unittest.skipIf(test_base.get_test_server_api() == 1,
"The tag API didn't work at v1 - see frontend issue #927") "The tag API didn't work at v1 - see frontend issue #927")

View file

@ -1,7 +1,10 @@
[tox] [tox]
envlist = py27,py26 envlist = py26, py27, py33
[testenv] [testenv]
commands = python -m unittest discover --catch
[testenv:py26]
commands = unit2 discover --catch commands = unit2 discover --catch
deps = deps =
unittest2 unittest2