diff --git a/.travis.yml b/.travis.yml index e8f456e..6891aed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,8 +7,9 @@ install: script: tox after_script: + # Install dependencies for Pylint + - pip install requests requests-oauthlib + # Run Pylint # (for information only, any errors don't affect the Travis result) - - pylint --use-ignore-patch=y openphoto - - pylint --use-ignore-patch=y tests - + - pylint --use-ignore-patch=y trovebox diff --git a/.travis/install_pylint b/.travis/install_pylint index e3ea6c2..0f5ce45 100755 --- a/.travis/install_pylint +++ b/.travis/install_pylint @@ -1,11 +1,10 @@ # Until the --use-ignore-patch makes it into pylint upstream, we need to # download and install from sneakypete81's pylint fork -HG_HASH=16de8b9518be -wget https://bitbucket.org/sneakypete81/pylint/get/$HG_HASH.zip -unzip $HG_HASH.zip -cd sneakypete81-pylint-$HG_HASH +wget https://bitbucket.org/sneakypete81/pylint/get/use_ignore_patch.zip +unzip use_ignore_patch.zip +cd sneakypete81-pylint-* python setup.py install cd .. -rm -r sneakypete81-pylint-$HG_HASH +rm -r sneakypete81-pylint-* diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..9466262 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,25 @@ +================================= +Trovebox Python Library Changelog +================================= + +v0.5 +==== + * Pylint improvements - using .pylint-ignores to waive warnings (#49) + * Add support for https URLs (#51) + * Configuration improvements (#53) + * Allow https SSL verification bypass (#50) + * Test improvements (#54) + +v0.4 +==== + +First release + + * Added more unit tests (#44, #45) + * Fixed consistency problems found with unit tests (#46) + * Renamed to Trovebox (#48) + * Packaged for PyPI: + - Updated metadata + - Store the version number inside the package, and add --version CLI option + - Update README and convert to ReStructuredText, as required by PyPI + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..9561fb1 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include README.rst diff --git a/README.rst b/README.rst index e66dba1..c656a89 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ Trovebox Python Library ======================= (Previously known as openphoto-python) -.. image:: https://api.travis-ci.org/photo/openphoto-python.png +.. image:: https://travis-ci.org/photo/openphoto-python.png?branch=master :alt: Build Status :target: https://travis-ci.org/photo/openphoto-python @@ -77,11 +77,16 @@ API Versioning ============== It may be useful to lock your application to a particular version of the Trovebox API. This ensures that future API updates won't cause unexpected breakages. +To do this, configure your Trovebox client as follows:: -To do this, add the optional ``api_version`` parameter when creating the client object:: + client.configure(api_version=2) - from trovebox import Trovebox - client = Trovebox(api_version=2) +SSL Verification +================ +If you connect to your Trovebox server over HTTPS, its SSL certificate is automatically verified. +You can configure your Trovebox client to bypass this verification step:: + + client.configure(ssl_verify=False) Commandline Tool ================ diff --git a/tests/README.markdown b/tests/README.markdown index 3aa3d35..4aef0d6 100644 --- a/tests/README.markdown +++ b/tests/README.markdown @@ -12,6 +12,7 @@ They run very quickly and don't require any external test hosts. #### Requirements * mock >= 1.0.0 * httpretty >= 0.6.1 + * ddt >= 0.3.0 * tox (optional) #### Running the Unit Tests @@ -19,7 +20,7 @@ They run very quickly and don't require any external test hosts. python -m unittest discover tests/unit To run the unit tests against all supported Python versions, use ```tox```: - + tox ---------------------------------------- diff --git a/tests/unit/test_config.py b/tests/unit/test_auth.py similarity index 71% rename from tests/unit/test_config.py rename to tests/unit/test_auth.py index a671bfe..bf70160 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_auth.py @@ -10,7 +10,7 @@ from trovebox import Trovebox CONFIG_HOME_PATH = os.path.join("tests", "config") CONFIG_PATH = os.path.join(CONFIG_HOME_PATH, "trovebox") -class TestConfig(unittest.TestCase): +class TestAuth(unittest.TestCase): def setUp(self): """ Override XDG_CONFIG_HOME env var, to use test configs """ try: @@ -42,47 +42,47 @@ class TestConfig(unittest.TestCase): """ Ensure the default config is loaded """ self.create_config("default", "Test Default Host") client = Trovebox() - config = client.config + auth = client.auth self.assertEqual(client.host, "Test Default Host") - self.assertEqual(config.consumer_key, "default_consumer_key") - self.assertEqual(config.consumer_secret, "default_consumer_secret") - self.assertEqual(config.token, "default_token") - self.assertEqual(config.token_secret, "default_token_secret") + self.assertEqual(auth.consumer_key, "default_consumer_key") + self.assertEqual(auth.consumer_secret, "default_consumer_secret") + self.assertEqual(auth.token, "default_token") + self.assertEqual(auth.token_secret, "default_token_secret") def test_custom_config(self): """ Ensure a custom config can be loaded """ self.create_config("default", "Test Default Host") self.create_config("custom", "Test Custom Host") client = Trovebox(config_file="custom") - config = client.config + auth = client.auth self.assertEqual(client.host, "Test Custom Host") - self.assertEqual(config.consumer_key, "custom_consumer_key") - self.assertEqual(config.consumer_secret, "custom_consumer_secret") - self.assertEqual(config.token, "custom_token") - self.assertEqual(config.token_secret, "custom_token_secret") + self.assertEqual(auth.consumer_key, "custom_consumer_key") + self.assertEqual(auth.consumer_secret, "custom_consumer_secret") + self.assertEqual(auth.token, "custom_token") + self.assertEqual(auth.token_secret, "custom_token_secret") def test_full_config_path(self): """ Ensure a full custom config path can be loaded """ self.create_config("path", "Test Path Host") full_path = os.path.abspath(CONFIG_PATH) client = Trovebox(config_file=os.path.join(full_path, "path")) - config = client.config + auth = client.auth self.assertEqual(client.host, "Test Path Host") - self.assertEqual(config.consumer_key, "path_consumer_key") - self.assertEqual(config.consumer_secret, "path_consumer_secret") - self.assertEqual(config.token, "path_token") - self.assertEqual(config.token_secret, "path_token_secret") + self.assertEqual(auth.consumer_key, "path_consumer_key") + self.assertEqual(auth.consumer_secret, "path_consumer_secret") + self.assertEqual(auth.token, "path_token") + self.assertEqual(auth.token_secret, "path_token_secret") def test_host_override(self): """ Ensure that specifying a host overrides the default config """ self.create_config("default", "Test Default Host") client = Trovebox(host="host_override") - config = client.config - self.assertEqual(config.host, "host_override") - self.assertEqual(config.consumer_key, "") - self.assertEqual(config.consumer_secret, "") - self.assertEqual(config.token, "") - self.assertEqual(config.token_secret, "") + auth = client.auth + self.assertEqual(auth.host, "host_override") + self.assertEqual(auth.consumer_key, "") + self.assertEqual(auth.consumer_secret, "") + self.assertEqual(auth.token, "") + self.assertEqual(auth.token_secret, "") def test_missing_config_files(self): """ Ensure that missing config files raise exceptions """ diff --git a/tests/unit/test_http.py b/tests/unit/test_http.py index 8f5ac3e..98883f1 100644 --- a/tests/unit/test_http.py +++ b/tests/unit/test_http.py @@ -1,7 +1,10 @@ from __future__ import unicode_literals import os import json +import mock import httpretty +from httpretty import GET, POST +from ddt import ddt, data try: import unittest2 as unittest # Python2.6 except ImportError: @@ -9,6 +12,21 @@ except ImportError: import trovebox +class GetOrPost(object): + """Helper class to call the correct (GET/POST) method""" + def __init__(self, client, method): + self.client = client + self.method = method + + def call(self, *args, **kwds): + if self.method == GET: + return self.client.get(*args, **kwds) + elif self.method == POST: + return self.client.post(*args, **kwds) + else: + raise ValueError("unknown method: %s" % self.method) + +@ddt class TestHttp(unittest.TestCase): test_host = "test.example.com" test_endpoint = "test.json" @@ -44,7 +62,55 @@ class TestHttp(unittest.TestCase): def test_attributes(self): """Check that the host attribute has been set correctly""" self.assertEqual(self.client.host, self.test_host) - self.assertEqual(self.client.config.host, self.test_host) + self.assertEqual(self.client.auth.host, self.test_host) + + @httpretty.activate + @data(GET, POST) + def test_http_scheme(self, method): + """Check that we can access hosts starting with 'http://'""" + self._register_uri(method, + uri="http://test.example.com/%s" % self.test_endpoint) + + self.client = trovebox.Trovebox(host="http://test.example.com", + **self.test_oauth) + response = GetOrPost(self.client, method).call(self.test_endpoint) + self.assertIn("OAuth", self._last_request().headers["authorization"]) + self.assertEqual(response, self.test_data) + self.assertEqual(self.client.last_url, + "http://test.example.com/%s" % self.test_endpoint) + self.assertEqual(self.client.last_response.json(), self.test_data) + + @httpretty.activate + @data(GET, POST) + def test_no_scheme(self, method): + """Check that we can access hosts without a 'http://' prefix""" + self._register_uri(method, + uri="http://test.example.com/%s" % self.test_endpoint) + + self.client = trovebox.Trovebox(host="test.example.com", + **self.test_oauth) + response = GetOrPost(self.client, method).call(self.test_endpoint) + self.assertIn("OAuth", self._last_request().headers["authorization"]) + self.assertEqual(response, self.test_data) + self.assertEqual(self.client.last_url, + "http://test.example.com/%s" % self.test_endpoint) + self.assertEqual(self.client.last_response.json(), self.test_data) + + @httpretty.activate + @data(GET, POST) + def test_https_scheme(self, method): + """Check that we can access hosts starting with 'https://'""" + self._register_uri(method, + uri="https://test.example.com/%s" % self.test_endpoint) + + self.client = trovebox.Trovebox(host="https://test.example.com", + **self.test_oauth) + response = GetOrPost(self.client, method).call(self.test_endpoint) + self.assertIn("OAuth", self._last_request().headers["authorization"]) + self.assertEqual(response, self.test_data) + self.assertEqual(self.client.last_url, + "https://test.example.com/%s" % self.test_endpoint) + self.assertEqual(self.client.last_response.json(), self.test_data) @httpretty.activate def test_get_with_parameters(self): @@ -93,17 +159,12 @@ class TestHttp(unittest.TestCase): self.client.post(self.test_endpoint) @httpretty.activate - def test_get_without_response_processing(self): - """Check that the get method works with response processing disabled""" - self._register_uri(httpretty.GET) - response = self.client.get(self.test_endpoint, process_response=False) - self.assertEqual(response, json.dumps(self.test_data)) - - @httpretty.activate - def test_post_without_response_processing(self): - """Check that the post method works with response processing disabled""" - self._register_uri(httpretty.POST) - response = self.client.post(self.test_endpoint, process_response=False) + @data(GET, POST) + def test_no_response_processing(self, method): + """Check that get/post methods work with response processing disabled""" + self._register_uri(method) + response = GetOrPost(self.client, method).call(self.test_endpoint, + process_response=False) self.assertEqual(response, json.dumps(self.test_data)) @httpretty.activate @@ -127,23 +188,31 @@ class TestHttp(unittest.TestCase): self.assertIn(params["unicode_"], [["\xc3\xbcmlaut"], ["\xfcmlaut"]]) @httpretty.activate - def test_get_with_api_version(self): - """Check that an API version can be specified for the get method""" - self.client = trovebox.Trovebox(host=self.test_host, api_version=1) - self._register_uri(httpretty.GET, + @data(GET, POST) + def test_api_version(self, method): + """Check that an API version can be specified""" + self.client = trovebox.Trovebox(host=self.test_host, **self.test_oauth) + self.client.configure(api_version=1) + self._register_uri(method, uri="http://%s/v1/%s" % (self.test_host, self.test_endpoint)) - self.client.get(self.test_endpoint) + GetOrPost(self.client, method).call(self.test_endpoint) - @httpretty.activate - def test_post_with_api_version(self): - """Check that an API version can be specified for the post method""" - self.client = trovebox.Trovebox(host=self.test_host, api_version=1, - **self.test_oauth) - self._register_uri(httpretty.POST, - uri="http://%s/v1/%s" % (self.test_host, - self.test_endpoint)) - self.client.post(self.test_endpoint) + @mock.patch.object(trovebox.http.requests, 'Session') + @data(GET, POST) + def test_ssl_verify_disabled(self, method, mock_session): + """Check that SSL verification can be disabled for the get method""" + session = mock_session.return_value.__enter__.return_value + session.get.return_value.text = "response text" + session.get.return_value.status_code = 200 + session.get.return_value.json.return_value = self.test_data + # Handle either post or get + session.post = session.get + + self.client = trovebox.Trovebox(host=self.test_host, **self.test_oauth) + self.client.configure(ssl_verify=False) + GetOrPost(self.client, method).call(self.test_endpoint) + self.assertEqual(session.verify, False) @httpretty.activate def test_post_file(self): diff --git a/tox.ini b/tox.ini index 2657d68..5e0c3da 100644 --- a/tox.ini +++ b/tox.ini @@ -6,11 +6,13 @@ commands = python -m unittest discover --catch tests/unit deps = mock >= 1.0.0 httpretty >= 0.6.1 + ddt >= 0.3.0 [testenv:py26] commands = unit2 discover --catch tests/unit deps = mock >= 1.0.0 httpretty >= 0.6.1 + ddt >= 0.3.0 unittest2 discover diff --git a/trovebox/.pylint-ignores.patch b/trovebox/.pylint-ignores.patch new file mode 100644 index 0000000..41fca5e --- /dev/null +++ b/trovebox/.pylint-ignores.patch @@ -0,0 +1,238 @@ +diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api_album.py patched/api_album.py +--- original/api_album.py 2013-08-16 18:12:30.434212000 +0100 ++++ patched/api_album.py 2013-08-16 18:13:29.678506001 +0100 +@@ -3,7 +3,7 @@ + """ + from .objects import Album + +-class ApiAlbums(object): ++class ApiAlbums(object): # pylint: disable=R0903,C0111 + def __init__(self, client): + self._client = client + +@@ -12,7 +12,7 @@ + results = self._client.get("/albums/list.json", **kwds)["result"] + return [Album(self._client, album) for album in results] + +-class ApiAlbum(object): ++class ApiAlbum(object): # pylint: disable=C0111 + def __init__(self, client): + self._client = client + +diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api_photo.py patched/api_photo.py +--- original/api_photo.py 2013-08-16 18:12:30.434212000 +0100 ++++ patched/api_photo.py 2013-08-16 18:13:29.678506001 +0100 +@@ -20,7 +20,7 @@ + ids.append(photo) + return ids + +-class ApiPhotos(object): ++class ApiPhotos(object): # pylint: disable=C0111 + def __init__(self, client): + self._client = client + +@@ -54,7 +54,7 @@ + raise TroveboxError("Delete response returned False") + return True + +-class ApiPhoto(object): ++class ApiPhoto(object): # pylint: disable=C0111 + def __init__(self, client): + self._client = client + +diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api_tag.py patched/api_tag.py +--- original/api_tag.py 2013-08-16 18:12:30.434212000 +0100 ++++ patched/api_tag.py 2013-08-16 18:13:29.678506001 +0100 +@@ -3,7 +3,7 @@ + """ + from .objects import Tag + +-class ApiTags(object): ++class ApiTags(object): # pylint: disable=R0903,C0111 + def __init__(self, client): + self._client = client + +@@ -12,7 +12,7 @@ + results = self._client.get("/tags/list.json", **kwds)["result"] + return [Tag(self._client, tag) for tag in results] + +-class ApiTag(object): ++class ApiTag(object): # pylint: disable=C0111 + def __init__(self, client): + self._client = client + +diff --unified --recursive '--exclude=.pylint-ignores.patch' original/auth.py patched/auth.py +--- original/auth.py 2013-08-16 18:13:24.966482000 +0100 ++++ patched/auth.py 2013-08-16 18:13:51.766615537 +0100 +@@ -4,7 +4,7 @@ + from __future__ import unicode_literals + import os + try: +- from configparser import ConfigParser # Python3 ++ from configparser import ConfigParser # Python3 # pylint: disable=F0401 + except ImportError: + from ConfigParser import SafeConfigParser as ConfigParser # Python2 + try: +@@ -12,9 +12,9 @@ + except ImportError: + import StringIO as io # Python2 + +-class Auth(object): ++class Auth(object): # pylint: disable=R0903 + """OAuth secrets""" +- def __init__(self, config_file, host, ++ def __init__(self, config_file, host, # pylint: disable=R0913 + consumer_key, consumer_secret, + token, token_secret): + if host is None: +@@ -69,7 +69,7 @@ + parser = ConfigParser() + parser.optionxform = str # Case-sensitive options + try: +- parser.read_file(buf) # Python3 ++ parser.read_file(buf) # Python3 # pylint: disable=E1103 + except AttributeError: + parser.readfp(buf) # Python2 + +diff --unified --recursive '--exclude=.pylint-ignores.patch' original/http.py patched/http.py +--- original/http.py 2013-08-16 17:54:30.688858000 +0100 ++++ patched/http.py 2013-08-16 18:14:14.106726301 +0100 +@@ -7,18 +7,18 @@ + import requests_oauthlib + import logging + try: +- from urllib.parse import urlparse, urlunparse # Python3 ++ from urllib.parse import urlparse, urlunparse # Python3 # pylint: disable=F0401,E0611 + except ImportError: + from urlparse import urlparse, urlunparse # Python2 + + from .objects import TroveboxObject +-from .errors import * ++from .errors import * # pylint: disable=W0401 + from .auth import Auth + + if sys.version < '3': +- TEXT_TYPE = unicode ++ TEXT_TYPE = unicode # pylint: disable=C0103 + else: +- TEXT_TYPE = str ++ TEXT_TYPE = str # pylint: disable=C0103 + + DUPLICATE_RESPONSE = {"code": 409, + "message": "This photo already exists"} +@@ -37,7 +37,7 @@ + "ssl_verify" : True, + } + +- def __init__(self, config_file=None, host=None, ++ def __init__(self, config_file=None, host=None, # pylint: disable=R0913 + consumer_key='', consumer_secret='', + token='', token_secret='', api_version=None): + +diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.py patched/__init__.py +--- original/__init__.py 2013-08-16 18:12:30.438212000 +0100 ++++ patched/__init__.py 2013-08-16 18:13:29.678506001 +0100 +@@ -2,7 +2,7 @@ + __init__.py : Trovebox package top level + """ + from .http import Http +-from .errors import * ++from .errors import * # pylint: disable=W0401 + from ._version import __version__ + from . import api_photo + from . import api_tag +@@ -22,7 +22,7 @@ + This should be used to ensure that your application will continue to work + even if the Trovebox API is updated to a new revision. + """ +- def __init__(self, config_file=None, host=None, ++ def __init__(self, config_file=None, host=None, # pylint: disable=R0913 + consumer_key='', consumer_secret='', + token='', token_secret='', + api_version=None): +diff --unified --recursive '--exclude=.pylint-ignores.patch' original/main.py patched/main.py +--- original/main.py 2013-08-16 18:12:30.438212000 +0100 ++++ patched/main.py 2013-08-16 18:13:29.678506001 +0100 +@@ -26,7 +26,7 @@ + + ################################################################# + +-def main(args=sys.argv[1:]): ++def main(args=sys.argv[1:]): # pylint: disable=R0912,C0111 + usage = "%prog --help" + parser = OptionParser(usage, add_help_option=False) + parser.add_option('-c', '--config', help="Configuration file to use", +@@ -84,13 +84,13 @@ + sys.exit(1) + + if options.method == "GET": +- result = client.get(options.endpoint, process_response=False, ++ result = client.get(options.endpoint, process_response=False, # pylint: disable=W0142 + **params) + else: + params, files = extract_files(params) +- result = client.post(options.endpoint, process_response=False, ++ result = client.post(options.endpoint, process_response=False, # pylint: disable=W0142 + files=files, **params) +- for f in files: ++ for f in files: # pylint: disable=C0103 + files[f].close() + + if options.verbose: +diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects.py patched/objects.py +--- original/objects.py 2013-08-16 18:12:30.438212000 +0100 ++++ patched/objects.py 2013-08-16 18:13:29.682506021 +0100 +@@ -2,16 +2,16 @@ + objects.py : Basic Trovebox API Objects + """ + try: +- from urllib.parse import quote # Python3 ++ from urllib.parse import quote # Python3 # pylint: disable=F0401,E0611 + except ImportError: + from urllib import quote # Python2 + + from .errors import TroveboxError + +-class TroveboxObject(object): ++class TroveboxObject(object): # pylint: disable=R0903 + """ Base object supporting the storage of custom fields as attributes """ + def __init__(self, trovebox, json_dict): +- self.id = None ++ self.id = None # pylint: disable=C0103 + self.name = None + self._trovebox = trovebox + self._json_dict = json_dict +@@ -57,7 +57,7 @@ + return self._json_dict + + +-class Photo(TroveboxObject): ++class Photo(TroveboxObject): # pylint: disable=C0111 + def delete(self, **kwds): + """ + Delete this photo. +@@ -147,7 +147,7 @@ + + self._replace_fields(new_dict) + +-class Tag(TroveboxObject): ++class Tag(TroveboxObject): # pylint: disable=C0111 + def delete(self, **kwds): + """ + Delete this tag. +@@ -168,7 +168,7 @@ + self._replace_fields(new_dict) + + +-class Album(TroveboxObject): ++class Album(TroveboxObject): # pylint: disable=C0111 + def __init__(self, trovebox, json_dict): + self.photos = None + self.cover = None +diff --unified --recursive '--exclude=.pylint-ignores.patch' original/_version.py patched/_version.py +--- original/_version.py 2013-08-16 18:12:30.438212000 +0100 ++++ patched/_version.py 2013-08-16 18:13:29.682506021 +0100 +@@ -1,2 +1,2 @@ +- ++ # pylint: disable=C0111 + __version__ = "0.4" diff --git a/trovebox/__init__.py b/trovebox/__init__.py index c1118e2..f8b0a28 100644 --- a/trovebox/__init__.py +++ b/trovebox/__init__.py @@ -1,3 +1,6 @@ +""" +__init__.py : Trovebox package top level +""" from .http import Http from .errors import * from ._version import __version__ diff --git a/trovebox/_version.py b/trovebox/_version.py index 896a370..ad57014 100644 --- a/trovebox/_version.py +++ b/trovebox/_version.py @@ -1 +1,2 @@ -__version__ = "0.4" + +__version__ = "0.5" diff --git a/trovebox/api_album.py b/trovebox/api_album.py index 9c212c0..4b69f87 100644 --- a/trovebox/api_album.py +++ b/trovebox/api_album.py @@ -1,6 +1,9 @@ +""" +api_album.py : Trovebox Album API Classes +""" from .objects import Album -class ApiAlbums: +class ApiAlbums(object): def __init__(self, client): self._client = client @@ -9,7 +12,7 @@ class ApiAlbums: results = self._client.get("/albums/list.json", **kwds)["result"] return [Album(self._client, album) for album in results] -class ApiAlbum: +class ApiAlbum(object): def __init__(self, client): self._client = client @@ -30,12 +33,15 @@ class ApiAlbum: return album.delete(**kwds) def form(self, album, **kwds): + """ Not yet implemented """ raise NotImplementedError() def add_photos(self, album, photos, **kwds): + """ Not yet implemented """ raise NotImplementedError() def remove_photos(self, album, photos, **kwds): + """ Not yet implemented """ raise NotImplementedError() def update(self, album, **kwds): diff --git a/trovebox/api_photo.py b/trovebox/api_photo.py index 6c1eacf..702a3de 100644 --- a/trovebox/api_photo.py +++ b/trovebox/api_photo.py @@ -1,3 +1,6 @@ +""" +api_photo.py : Trovebox Photo API Classes +""" import base64 from .errors import TroveboxError @@ -17,7 +20,7 @@ def extract_ids(photos): ids.append(photo) return ids -class ApiPhotos: +class ApiPhotos(object): def __init__(self, client): self._client = client @@ -51,7 +54,7 @@ class ApiPhotos: raise TroveboxError("Delete response returned False") return True -class ApiPhoto: +class ApiPhoto(object): def __init__(self, client): self._client = client @@ -72,9 +75,11 @@ class ApiPhoto: return photo.edit(**kwds) def replace(self, photo, photo_file, **kwds): + """ Not yet implemented """ raise NotImplementedError() def replace_encoded(self, photo, photo_file, **kwds): + """ Not yet implemented """ raise NotImplementedError() def update(self, photo, **kwds): @@ -114,6 +119,7 @@ class ApiPhoto: return Photo(self._client, result) def dynamic_url(self, photo, **kwds): + """ Not yet implemented """ raise NotImplementedError() def next_previous(self, photo, **kwds): diff --git a/trovebox/api_tag.py b/trovebox/api_tag.py index 4221c9a..0a694a6 100644 --- a/trovebox/api_tag.py +++ b/trovebox/api_tag.py @@ -1,6 +1,9 @@ +""" +api_tag.py : Trovebox Tag API Classes +""" from .objects import Tag -class ApiTags: +class ApiTags(object): def __init__(self, client): self._client = client @@ -9,7 +12,7 @@ class ApiTags: results = self._client.get("/tags/list.json", **kwds)["result"] return [Tag(self._client, tag) for tag in results] -class ApiTag: +class ApiTag(object): def __init__(self, client): self._client = client diff --git a/trovebox/config.py b/trovebox/auth.py similarity index 96% rename from trovebox/config.py rename to trovebox/auth.py index bacef2b..378ee65 100644 --- a/trovebox/config.py +++ b/trovebox/auth.py @@ -1,3 +1,6 @@ +""" +auth.py : OAuth Config File Parser +""" from __future__ import unicode_literals import os try: @@ -9,7 +12,8 @@ try: except ImportError: import StringIO as io # Python2 -class Config: +class Auth(object): + """OAuth secrets""" def __init__(self, config_file, host, consumer_key, consumer_secret, token, token_secret): @@ -46,7 +50,8 @@ def get_config_path(config_file): def read_config(config_path): """ Loads config data from the specified file path. - If config_file doesn't exist, returns an empty authentication config for localhost. + If config_file doesn't exist, returns an empty authentication config + for localhost. """ section = "DUMMY" defaults = {'host': 'localhost', diff --git a/trovebox/errors.py b/trovebox/errors.py index c813e5a..15b13cc 100644 --- a/trovebox/errors.py +++ b/trovebox/errors.py @@ -1,3 +1,6 @@ +""" +errors.py : Trovebox Error Classes +""" class TroveboxError(Exception): """ Indicates that an Trovebox operation failed """ pass diff --git a/trovebox/http.py b/trovebox/http.py index fef1fdc..4cc0faf 100644 --- a/trovebox/http.py +++ b/trovebox/http.py @@ -1,16 +1,19 @@ +""" +http.py : Trovebox HTTP Access +""" from __future__ import unicode_literals import sys import requests import requests_oauthlib import logging try: - from urllib.parse import urlunparse # Python3 + from urllib.parse import urlparse, urlunparse # Python3 except ImportError: - from urlparse import urlunparse # Python2 + from urlparse import urlparse, urlunparse # Python2 from .objects import TroveboxObject from .errors import * -from .config import Config +from .auth import Auth if sys.version < '3': TEXT_TYPE = unicode @@ -20,36 +23,58 @@ else: DUPLICATE_RESPONSE = {"code": 409, "message": "This photo already exists"} -class Http: +class Http(object): """ Base class to handle HTTP requests to an Trovebox server. - If no parameters are specified, config is loaded from the default - location (~/.config/trovebox/default). + If no parameters are specified, auth config is loaded from the + default location (~/.config/trovebox/default). The config_file parameter is used to specify an alternate config file. 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 Trovebox API is updated to a new revision. """ + + _CONFIG_DEFAULTS = {"api_version" : None, + "ssl_verify" : True, + } + def __init__(self, config_file=None, host=None, consumer_key='', consumer_secret='', token='', token_secret='', api_version=None): - self._api_version = api_version + + self.config = dict(self._CONFIG_DEFAULTS) + + if api_version is not None: + print("Deprecation Warning: api_version should be set by " + "calling the configure function") + self.config["api_version"] = api_version self._logger = logging.getLogger("trovebox") - self.config = Config(config_file, host, - consumer_key, consumer_secret, - token, token_secret) + self.auth = Auth(config_file, host, + consumer_key, consumer_secret, + token, token_secret) - self.host = self.config.host + self.host = self.auth.host # Remember the most recent HTTP request and response self.last_url = None self.last_params = None self.last_response = None + def configure(self, **kwds): + """ + Update Trovebox HTTP client configuration. + + :param api_version: Include a Trovebox API version in all requests. + This can be used to ensure that your application will continue + to work even if the Trovebox API is updated to a new revision. + [default: None] + :param ssl_verify: If true, HTTPS SSL certificates will always be + verified [default: True] + """ + for item in kwds: + self.config[item] = kwds[item] + def get(self, endpoint, process_response=True, **params): """ Performs an HTTP GET from the specified endpoint (API path), @@ -62,21 +87,18 @@ class Http: 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 = urlunparse(('http', self.host, endpoint, '', '', '')) + url = self._construct_url(endpoint) - if self.config.consumer_key: - auth = requests_oauthlib.OAuth1(self.config.consumer_key, - self.config.consumer_secret, - self.config.token, - self.config.token_secret) + if self.auth.consumer_key: + auth = requests_oauthlib.OAuth1(self.auth.consumer_key, + self.auth.consumer_secret, + self.auth.token, + self.auth.token_secret) else: auth = None with requests.Session() as session: + session.verify = self.config["ssl_verify"] response = session.get(url, params=params, auth=auth) self._logger.info("============================") @@ -105,20 +127,17 @@ class Http: 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 = urlunparse(('http', self.host, endpoint, '', '', '')) + url = self._construct_url(endpoint) - if not self.config.consumer_key: + if not self.auth.consumer_key: raise TroveboxError("Cannot issue POST without OAuth tokens") - auth = requests_oauthlib.OAuth1(self.config.consumer_key, - self.config.consumer_secret, - self.config.token, - self.config.token_secret) + auth = requests_oauthlib.OAuth1(self.auth.consumer_key, + self.auth.consumer_secret, + self.auth.token, + self.auth.token_secret) with requests.Session() as session: + session.verify = self.config["ssl_verify"] if files: # Need to pass parameters as URL query, so they get OAuth signed response = session.post(url, params=params, @@ -146,6 +165,22 @@ class Http: else: return response.text + def _construct_url(self, endpoint): + """Return the full URL to the specified endpoint""" + parsed_url = urlparse(self.host) + scheme = parsed_url[0] + host = parsed_url[1] + # Handle host without a scheme specified (eg. www.example.com) + if scheme == "": + scheme = "http" + host = self.host + + if not endpoint.startswith("/"): + endpoint = "/" + endpoint + if self.config["api_version"] is not None: + endpoint = "/v%d%s" % (self.config["api_version"], endpoint) + return urlunparse((scheme, host, endpoint, '', '', '')) + @staticmethod def _process_params(params): """ Converts Unicode/lists/booleans inside HTTP parameters """ diff --git a/trovebox/main.py b/trovebox/main.py index dfe2573..b7b84e4 100644 --- a/trovebox/main.py +++ b/trovebox/main.py @@ -1,4 +1,7 @@ #!/usr/bin/env python +""" +main.py : Trovebox Console Script +""" import os import sys import json @@ -44,7 +47,7 @@ def main(args=sys.argv[1:]): action="store_true", dest="pretty", default=False) parser.add_option('-v', help="Verbose output", action="store_true", dest="verbose", default=False) - parser.add_option('--version', help="Display the current version information", + parser.add_option('--version', help="Display the current version", action="store_true") parser.add_option('--help', help='show this help message', action="store_true") @@ -107,7 +110,8 @@ def main(args=sys.argv[1:]): def extract_files(params): """ - Extract filenames from the "photo" parameter, so they can be uploaded, returning (updated_params, files). + Extract filenames from the "photo" parameter so they can be uploaded, + returning (updated_params, files). Uses the same technique as the Trovebox PHP commandline tool: * Filename can only be in the "photo" parameter * Filename must be prefixed with "@" diff --git a/trovebox/objects.py b/trovebox/objects.py index ffced54..7c1335a 100644 --- a/trovebox/objects.py +++ b/trovebox/objects.py @@ -1,3 +1,6 @@ +""" +objects.py : Basic Trovebox API Objects +""" try: from urllib.parse import quote # Python3 except ImportError: @@ -5,7 +8,7 @@ except ImportError: from .errors import TroveboxError -class TroveboxObject: +class TroveboxObject(object): """ Base object supporting the storage of custom fields as attributes """ def __init__(self, trovebox, json_dict): self.id = None @@ -75,9 +78,11 @@ class Photo(TroveboxObject): return result["markup"] def replace(self, photo_file, **kwds): + """ Not implemented yet """ raise NotImplementedError() def replace_encoded(self, photo_file, **kwds): + """ Not implemented yet """ raise NotImplementedError() def update(self, **kwds): @@ -96,6 +101,7 @@ class Photo(TroveboxObject): self._replace_fields(new_dict) def dynamic_url(self, **kwds): + """ Not implemented yet """ raise NotImplementedError() def next_previous(self, **kwds): @@ -194,12 +200,15 @@ class Album(TroveboxObject): return result def form(self, **kwds): + """ Not implemented yet """ raise NotImplementedError() def add_photos(self, photos, **kwds): + """ Not implemented yet """ raise NotImplementedError() def remove_photos(self, photos, **kwds): + """ Not implemented yet """ raise NotImplementedError() def update(self, **kwds):