diff --git a/openphoto/__init__.py b/openphoto/__init__.py index ce569c5..67eb995 100644 --- a/openphoto/__init__.py +++ b/openphoto/__init__.py @@ -1,8 +1,8 @@ -from openphoto_http import OpenPhotoHttp -from errors import * -import api_photo -import api_tag -import api_album +from .openphoto_http import OpenPhotoHttp +from .errors import * +from . import api_photo +from . import api_tag +from . import api_album LATEST_API_VERSION = 2 diff --git a/openphoto/api_album.py b/openphoto/api_album.py index 9777a30..96a31b3 100644 --- a/openphoto/api_album.py +++ b/openphoto/api_album.py @@ -1,5 +1,5 @@ -from errors import * -from objects import Album +from .errors import * +from .objects import Album class ApiAlbums: def __init__(self, client): diff --git a/openphoto/api_photo.py b/openphoto/api_photo.py index 221996c..033febc 100644 --- a/openphoto/api_photo.py +++ b/openphoto/api_photo.py @@ -1,7 +1,7 @@ import base64 -from errors import * -from objects import Photo +from .errors import * +from .objects import Photo class ApiPhotos: def __init__(self, client): @@ -80,14 +80,16 @@ class ApiPhoto: return photo def upload(self, photo_file, **kwds): - result = self._client.post("/photo/upload.json", - files={'photo': open(photo_file, 'rb')}, - **kwds)["result"] + with open(photo_file, 'rb') as f: + result = self._client.post("/photo/upload.json", + files={'photo': f}, + **kwds)["result"] return Photo(self._client, result) def upload_encoded(self, photo_file, **kwds): """ 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, **kwds)["result"] return Photo(self._client, result) diff --git a/openphoto/api_tag.py b/openphoto/api_tag.py index bc87d32..be393c0 100644 --- a/openphoto/api_tag.py +++ b/openphoto/api_tag.py @@ -1,5 +1,5 @@ -from errors import * -from objects import Tag +from .errors import * +from .objects import Tag class ApiTags: def __init__(self, client): diff --git a/openphoto/main.py b/openphoto/main.py index 920262e..a2f944b 100644 --- a/openphoto/main.py +++ b/openphoto/main.py @@ -2,7 +2,6 @@ import os import sys import string -import urllib from optparse import OptionParser try: @@ -56,22 +55,22 @@ def main(args=sys.argv[1:]): try: client = OpenPhoto(config_file=options.config_file) except IOError as error: - print error - print - print "You must create a configuration file with the following contents:" - print " host = your.host.com" - print " consumerKey = your_consumer_key" - print " consumerSecret = your_consumer_secret" - print " token = your_access_token" - print " tokenSecret = your_access_token_secret" - print - print "To get your credentials:" - print " * Log into your Trovebox site" - print " * Click the arrow on the top-right and select 'Settings'." - print " * Click the 'Create a new app' button." - print " * Click the 'View' link beside the newly created app." - print - print error + print(error) + print() + print("You must create a configuration file with the following contents:") + print(" host = your.host.com") + print(" consumerKey = your_consumer_key") + print(" consumerSecret = your_consumer_secret") + print(" token = your_access_token") + print(" tokenSecret = your_access_token_secret") + print() + print("To get your credentials:") + print(" * Log into your Trovebox site") + print(" * Click the arrow on the top-right and select 'Settings'.") + print(" * Click the 'Create a new app' button.") + print(" * Click the 'View' link beside the newly created app.") + print() + print(error) sys.exit(1) 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) 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: - print "Fields:" - for kv in params.iteritems(): - print " %s=%s" % kv - print "==========\n" + print("Fields:") + for kv in params.items(): + print(" %s=%s" % kv) + print("==========\n") 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: - print result + print(result) def extract_files(params): """ diff --git a/openphoto/objects.py b/openphoto/objects.py index 155c711..f333847 100644 --- a/openphoto/objects.py +++ b/openphoto/objects.py @@ -1,5 +1,8 @@ -import urllib -from errors import * +try: + from urllib.parse import quote # Python3 +except ImportError: + from urllib import quote # Python2 +from .errors import * class OpenPhotoObject: """ Base object supporting the storage of custom fields as attributes """ @@ -128,13 +131,13 @@ class Tag(OpenPhotoObject): Returns True if successful. 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({}) return result def update(self, **kwds): """ 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"] self._replace_fields(new_dict) diff --git a/openphoto/openphoto_http.py b/openphoto/openphoto_http.py index 17919a2..7824bad 100644 --- a/openphoto/openphoto_http.py +++ b/openphoto/openphoto_http.py @@ -1,18 +1,36 @@ +from __future__ import unicode_literals +import sys import os -import urlparse -import urllib +try: + from urllib.parse import urlunparse # Python3 +except ImportError: + from urlparse import urlunparse # Python2 import requests import requests_oauthlib import logging -import StringIO -import ConfigParser try: - import json + import io # Python3 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 -from errors import * +if sys.version < '3': + 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, "message": "This photo already exists"} @@ -75,15 +93,17 @@ class OpenPhotoHttp: 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 = urlunparse(('http', self._host, endpoint, '', '', '')) if self._consumer_key: auth = requests_oauthlib.OAuth1(self._consumer_key, self._consumer_secret, - self._token, self._token_secret) + self._token, self._token_secret, + decoding=_oauth_decoding) else: 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("GET %s" % url) @@ -115,20 +135,22 @@ class OpenPhotoHttp: 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 = urlunparse(('http', self._host, endpoint, '', '', '')) if not self._consumer_key: raise OpenPhotoError("Cannot issue POST without OAuth tokens") auth = requests_oauthlib.OAuth1(self._consumer_key, self._consumer_secret, - self._token, self._token_secret) - if files: - # Need to pass parameters as URL query, so they get OAuth signed - response = requests.post(url, params=params, files=files, auth=auth) - else: - # Passing parameters as URL query doesn't work if there are no files to send. - # Send them as form data instead. - response = requests.post(url, data=params, auth=auth) + self._token, self._token_secret, + decoding=_oauth_decoding) + with requests.Session() as s: + if files: + # Need to pass parameters as URL query, so they get OAuth signed + response = s.post(url, params=params, files=files, auth=auth) + else: + # Passing parameters as URL query doesn't work if there are no files to send. + # Send them as form data instead. + response = s.post(url, data=params, auth=auth) self._logger.info("============================") self._logger.info("POST %s" % url) @@ -156,9 +178,9 @@ class OpenPhotoHttp: if isinstance(value, OpenPhotoObject): value = value.id - # Use UTF-8 encoding - if isinstance(value, unicode): - value = value.encode('utf-8') + # Ensure value is UTF-8 encoded + if isinstance(value, text_type): + value = value.encode("utf-8") # Handle lists if isinstance(value, list): @@ -168,8 +190,8 @@ class OpenPhotoHttp: for i, item in enumerate(new_list): if isinstance(item, OpenPhotoObject): new_list[i] = item.id - # Convert list to unicode string - value = u','.join([unicode(item) for item in new_list]) + # Convert list to string + value = ','.join([str(item) for item in new_list]) # Handle booleans if isinstance(value, bool): @@ -188,7 +210,7 @@ class OpenPhotoHttp: json_response = response.json() code = json_response["code"] message = json_response["message"] - except ValueError, KeyError: + except (ValueError, KeyError): # Response wasn't OpenPhoto JSON - check the HTTP status code if 200 <= response.status_code < 300: # Status code was valid, so just reraise the exception @@ -236,14 +258,18 @@ class OpenPhotoHttp: 'token': '', 'tokenSecret':'', } # 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(open(config_file).read()) + with io.open(config_file, "r") as f: + buf.write(f.read()) buf.seek(0, os.SEEK_SET) - parser = ConfigParser.SafeConfigParser() + parser = ConfigParser() parser.optionxform = str # Case-sensitive options - parser.readfp(buf) + try: + parser.read_file(buf) # Python3 + except AttributeError: + parser.readfp(buf) # Python2 # Trim quotes config = parser.items(section) diff --git a/tests/test_albums.py b/tests/test_albums.py index 0371bea..88eeb19 100644 --- a/tests/test_albums.py +++ b/tests/test_albums.py @@ -3,7 +3,7 @@ try: except ImportError: import unittest import openphoto -import test_base +from . import test_base class TestAlbums(test_base.TestBase): testcase_name = "album API" diff --git a/tests/test_base.py b/tests/test_base.py index ae5d350..86a27fe 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,3 +1,4 @@ +from __future__ import print_function import sys import os try: @@ -35,9 +36,9 @@ class TestBase(unittest.TestCase): """ Ensure there is nothing on the server before running any tests """ if cls.debug: if cls.api_version is None: - print "\nTesting Latest %s" % cls.testcase_name + print("\nTesting Latest %s" % cls.testcase_name) 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, api_version=cls.api_version) @@ -71,9 +72,9 @@ class TestBase(unittest.TestCase): self.photos = self.client.photos.list() if len(self.photos) != 3: if self.debug: - print "[Regenerating Photos]" + print("[Regenerating Photos]") else: - print " ", + print(" ", end='') sys.stdout.flush() if len(self.photos) > 0: self._delete_all() @@ -85,16 +86,16 @@ class TestBase(unittest.TestCase): self.tags[0].id != self.TEST_TAG or str(self.tags[0].count) != "3"): if self.debug: - print "[Regenerating Tags]" + print("[Regenerating Tags]") else: - print " ", + print(" ", end='') sys.stdout.flush() self._delete_all() self._create_test_photos() self.photos = self.client.photos.list() self.tags = self.client.tags.list() if len(self.tags) != 1: - print "Tags: %s" % self.tags + print("Tags: %s" % self.tags) raise Exception("Tag creation failed") 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].count != "3"): if self.debug: - print "[Regenerating Albums]" + print("[Regenerating Albums]") else: - print " ", + print(" ", end='') sys.stdout.flush() self._delete_all() self._create_test_photos() @@ -112,7 +113,7 @@ class TestBase(unittest.TestCase): self.tags = self.client.tags.list() self.albums = self.client.albums.list() if len(self.albums) != 1: - print "Albums: %s" % self.albums + print("Albums: %s" % self.albums) raise Exception("Album creation failed") logging.info("\nRunning %s..." % self.id()) diff --git a/tests/test_config.py b/tests/test_config.py index de0b267..d0d21c2 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -27,13 +27,13 @@ class TestConfig(unittest.TestCase): shutil.rmtree(CONFIG_HOME_PATH, ignore_errors=True) def create_config(self, config_file, host): - f = open(os.path.join(CONFIG_PATH, config_file), "w") - f.write("host = %s\n" % host) - f.write("# Comment\n\n") - f.write("consumerKey = \"%s_consumer_key\"\n" % config_file) - f.write("\"consumerSecret\" = %s_consumer_secret\n" % config_file) - f.write("'token'=%s_token\n" % config_file) - f.write("tokenSecret = '%s_token_secret'\n" % config_file) + with open(os.path.join(CONFIG_PATH, config_file), "w") as f: + f.write("host = %s\n" % host) + f.write("# Comment\n\n") + f.write("consumerKey = \"%s_consumer_key\"\n" % config_file) + f.write("\"consumerSecret\" = %s_consumer_secret\n" % config_file) + f.write("'token'=%s_token\n" % config_file) + f.write("tokenSecret = '%s_token_secret'\n" % config_file) def test_default_config(self): """ Ensure the default config is loaded """ diff --git a/tests/test_framework.py b/tests/test_framework.py index cd1700a..d011914 100644 --- a/tests/test_framework.py +++ b/tests/test_framework.py @@ -4,7 +4,7 @@ except ImportError: import unittest import logging import openphoto -import test_base +from . import test_base class TestFramework(test_base.TestBase): testcase_name = "framework" diff --git a/tests/test_photos.py b/tests/test_photos.py index dcd9c1e..0772d86 100644 --- a/tests/test_photos.py +++ b/tests/test_photos.py @@ -1,9 +1,10 @@ +from __future__ import unicode_literals try: import unittest2 as unittest except ImportError: import unittest import openphoto -import test_base +from . import test_base class TestPhotos(test_base.TestBase): testcase_name = "photo API" @@ -72,7 +73,7 @@ class TestPhotos(test_base.TestBase): def test_update(self): """ 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 photo = self.photos[0] self.assertNotEqual(photo.title, title) diff --git a/tests/test_tags.py b/tests/test_tags.py index 143742e..a46f1cc 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -3,7 +3,7 @@ try: except ImportError: import unittest import openphoto -import test_base +from . import test_base @unittest.skipIf(test_base.get_test_server_api() == 1, "The tag API didn't work at v1 - see frontend issue #927") diff --git a/tox.ini b/tox.ini index 23a69ed..7f1f87f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,10 @@ [tox] -envlist = py27,py26 +envlist = py26, py27, py33 [testenv] +commands = python -m unittest discover --catch + +[testenv:py26] commands = unit2 discover --catch deps = unittest2