Merge branch 'release-0.5'

This commit is contained in:
sneakypete81 2013-08-18 19:23:36 +01:00
commit ed5cc0ac11
20 changed files with 523 additions and 107 deletions

View file

@ -7,8 +7,9 @@ install:
script: tox script: tox
after_script: after_script:
# Install dependencies for Pylint
- pip install requests requests-oauthlib
# Run Pylint # Run Pylint
# (for information only, any errors don't affect the Travis result) # (for information only, any errors don't affect the Travis result)
- pylint --use-ignore-patch=y openphoto - pylint --use-ignore-patch=y trovebox
- pylint --use-ignore-patch=y tests

View file

@ -1,11 +1,10 @@
# Until the --use-ignore-patch makes it into pylint upstream, we need to # Until the --use-ignore-patch makes it into pylint upstream, we need to
# download and install from sneakypete81's pylint fork # download and install from sneakypete81's pylint fork
HG_HASH=16de8b9518be
wget https://bitbucket.org/sneakypete81/pylint/get/$HG_HASH.zip wget https://bitbucket.org/sneakypete81/pylint/get/use_ignore_patch.zip
unzip $HG_HASH.zip unzip use_ignore_patch.zip
cd sneakypete81-pylint-$HG_HASH cd sneakypete81-pylint-*
python setup.py install python setup.py install
cd .. cd ..
rm -r sneakypete81-pylint-$HG_HASH rm -r sneakypete81-pylint-*

25
CHANGELOG Normal file
View file

@ -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

1
MANIFEST.in Normal file
View file

@ -0,0 +1 @@
include README.rst

View file

@ -3,7 +3,7 @@ Trovebox Python Library
======================= =======================
(Previously known as openphoto-python) (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 :alt: Build Status
:target: https://travis-ci.org/photo/openphoto-python :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. 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. 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 SSL Verification
client = Trovebox(api_version=2) ================
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 Commandline Tool
================ ================

View file

@ -12,6 +12,7 @@ They run very quickly and don't require any external test hosts.
#### Requirements #### Requirements
* mock >= 1.0.0 * mock >= 1.0.0
* httpretty >= 0.6.1 * httpretty >= 0.6.1
* ddt >= 0.3.0
* tox (optional) * tox (optional)
#### Running the Unit Tests #### Running the Unit Tests

View file

@ -10,7 +10,7 @@ from trovebox import Trovebox
CONFIG_HOME_PATH = os.path.join("tests", "config") CONFIG_HOME_PATH = os.path.join("tests", "config")
CONFIG_PATH = os.path.join(CONFIG_HOME_PATH, "trovebox") CONFIG_PATH = os.path.join(CONFIG_HOME_PATH, "trovebox")
class TestConfig(unittest.TestCase): class TestAuth(unittest.TestCase):
def setUp(self): def setUp(self):
""" Override XDG_CONFIG_HOME env var, to use test configs """ """ Override XDG_CONFIG_HOME env var, to use test configs """
try: try:
@ -42,47 +42,47 @@ class TestConfig(unittest.TestCase):
""" Ensure the default config is loaded """ """ Ensure the default config is loaded """
self.create_config("default", "Test Default Host") self.create_config("default", "Test Default Host")
client = Trovebox() client = Trovebox()
config = client.config auth = client.auth
self.assertEqual(client.host, "Test Default Host") self.assertEqual(client.host, "Test Default Host")
self.assertEqual(config.consumer_key, "default_consumer_key") self.assertEqual(auth.consumer_key, "default_consumer_key")
self.assertEqual(config.consumer_secret, "default_consumer_secret") self.assertEqual(auth.consumer_secret, "default_consumer_secret")
self.assertEqual(config.token, "default_token") self.assertEqual(auth.token, "default_token")
self.assertEqual(config.token_secret, "default_token_secret") self.assertEqual(auth.token_secret, "default_token_secret")
def test_custom_config(self): def test_custom_config(self):
""" Ensure a custom config can be loaded """ """ Ensure a custom config can be loaded """
self.create_config("default", "Test Default Host") self.create_config("default", "Test Default Host")
self.create_config("custom", "Test Custom Host") self.create_config("custom", "Test Custom Host")
client = Trovebox(config_file="custom") client = Trovebox(config_file="custom")
config = client.config auth = client.auth
self.assertEqual(client.host, "Test Custom Host") self.assertEqual(client.host, "Test Custom Host")
self.assertEqual(config.consumer_key, "custom_consumer_key") self.assertEqual(auth.consumer_key, "custom_consumer_key")
self.assertEqual(config.consumer_secret, "custom_consumer_secret") self.assertEqual(auth.consumer_secret, "custom_consumer_secret")
self.assertEqual(config.token, "custom_token") self.assertEqual(auth.token, "custom_token")
self.assertEqual(config.token_secret, "custom_token_secret") self.assertEqual(auth.token_secret, "custom_token_secret")
def test_full_config_path(self): def test_full_config_path(self):
""" Ensure a full custom config path can be loaded """ """ Ensure a full custom config path can be loaded """
self.create_config("path", "Test Path Host") self.create_config("path", "Test Path Host")
full_path = os.path.abspath(CONFIG_PATH) full_path = os.path.abspath(CONFIG_PATH)
client = Trovebox(config_file=os.path.join(full_path, "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(client.host, "Test Path Host")
self.assertEqual(config.consumer_key, "path_consumer_key") self.assertEqual(auth.consumer_key, "path_consumer_key")
self.assertEqual(config.consumer_secret, "path_consumer_secret") self.assertEqual(auth.consumer_secret, "path_consumer_secret")
self.assertEqual(config.token, "path_token") self.assertEqual(auth.token, "path_token")
self.assertEqual(config.token_secret, "path_token_secret") self.assertEqual(auth.token_secret, "path_token_secret")
def test_host_override(self): def test_host_override(self):
""" Ensure that specifying a host overrides the default config """ """ Ensure that specifying a host overrides the default config """
self.create_config("default", "Test Default Host") self.create_config("default", "Test Default Host")
client = Trovebox(host="host_override") client = Trovebox(host="host_override")
config = client.config auth = client.auth
self.assertEqual(config.host, "host_override") self.assertEqual(auth.host, "host_override")
self.assertEqual(config.consumer_key, "") self.assertEqual(auth.consumer_key, "")
self.assertEqual(config.consumer_secret, "") self.assertEqual(auth.consumer_secret, "")
self.assertEqual(config.token, "") self.assertEqual(auth.token, "")
self.assertEqual(config.token_secret, "") self.assertEqual(auth.token_secret, "")
def test_missing_config_files(self): def test_missing_config_files(self):
""" Ensure that missing config files raise exceptions """ """ Ensure that missing config files raise exceptions """

View file

@ -1,7 +1,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
import json import json
import mock
import httpretty import httpretty
from httpretty import GET, POST
from ddt import ddt, data
try: try:
import unittest2 as unittest # Python2.6 import unittest2 as unittest # Python2.6
except ImportError: except ImportError:
@ -9,6 +12,21 @@ except ImportError:
import trovebox 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): class TestHttp(unittest.TestCase):
test_host = "test.example.com" test_host = "test.example.com"
test_endpoint = "test.json" test_endpoint = "test.json"
@ -44,7 +62,55 @@ class TestHttp(unittest.TestCase):
def test_attributes(self): def test_attributes(self):
"""Check that the host attribute has been set correctly""" """Check that the host attribute has been set correctly"""
self.assertEqual(self.client.host, self.test_host) 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 @httpretty.activate
def test_get_with_parameters(self): def test_get_with_parameters(self):
@ -93,17 +159,12 @@ class TestHttp(unittest.TestCase):
self.client.post(self.test_endpoint) self.client.post(self.test_endpoint)
@httpretty.activate @httpretty.activate
def test_get_without_response_processing(self): @data(GET, POST)
"""Check that the get method works with response processing disabled""" def test_no_response_processing(self, method):
self._register_uri(httpretty.GET) """Check that get/post methods work with response processing disabled"""
response = self.client.get(self.test_endpoint, process_response=False) self._register_uri(method)
self.assertEqual(response, json.dumps(self.test_data)) response = GetOrPost(self.client, method).call(self.test_endpoint,
process_response=False)
@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)
self.assertEqual(response, json.dumps(self.test_data)) self.assertEqual(response, json.dumps(self.test_data))
@httpretty.activate @httpretty.activate
@ -127,23 +188,31 @@ class TestHttp(unittest.TestCase):
self.assertIn(params["unicode_"], [["\xc3\xbcmlaut"], ["\xfcmlaut"]]) self.assertIn(params["unicode_"], [["\xc3\xbcmlaut"], ["\xfcmlaut"]])
@httpretty.activate @httpretty.activate
def test_get_with_api_version(self): @data(GET, POST)
"""Check that an API version can be specified for the get method""" def test_api_version(self, method):
self.client = trovebox.Trovebox(host=self.test_host, api_version=1) """Check that an API version can be specified"""
self._register_uri(httpretty.GET, 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, uri="http://%s/v1/%s" % (self.test_host,
self.test_endpoint)) self.test_endpoint))
self.client.get(self.test_endpoint) GetOrPost(self.client, method).call(self.test_endpoint)
@httpretty.activate @mock.patch.object(trovebox.http.requests, 'Session')
def test_post_with_api_version(self): @data(GET, POST)
"""Check that an API version can be specified for the post method""" def test_ssl_verify_disabled(self, method, mock_session):
self.client = trovebox.Trovebox(host=self.test_host, api_version=1, """Check that SSL verification can be disabled for the get method"""
**self.test_oauth) session = mock_session.return_value.__enter__.return_value
self._register_uri(httpretty.POST, session.get.return_value.text = "response text"
uri="http://%s/v1/%s" % (self.test_host, session.get.return_value.status_code = 200
self.test_endpoint)) session.get.return_value.json.return_value = self.test_data
self.client.post(self.test_endpoint) # 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 @httpretty.activate
def test_post_file(self): def test_post_file(self):

View file

@ -6,11 +6,13 @@ commands = python -m unittest discover --catch tests/unit
deps = deps =
mock >= 1.0.0 mock >= 1.0.0
httpretty >= 0.6.1 httpretty >= 0.6.1
ddt >= 0.3.0
[testenv:py26] [testenv:py26]
commands = unit2 discover --catch tests/unit commands = unit2 discover --catch tests/unit
deps = deps =
mock >= 1.0.0 mock >= 1.0.0
httpretty >= 0.6.1 httpretty >= 0.6.1
ddt >= 0.3.0
unittest2 unittest2
discover discover

View file

@ -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"

View file

@ -1,3 +1,6 @@
"""
__init__.py : Trovebox package top level
"""
from .http import Http from .http import Http
from .errors import * from .errors import *
from ._version import __version__ from ._version import __version__

View file

@ -1 +1,2 @@
__version__ = "0.4"
__version__ = "0.5"

View file

@ -1,6 +1,9 @@
"""
api_album.py : Trovebox Album API Classes
"""
from .objects import Album from .objects import Album
class ApiAlbums: class ApiAlbums(object):
def __init__(self, client): def __init__(self, client):
self._client = client self._client = client
@ -9,7 +12,7 @@ class ApiAlbums:
results = self._client.get("/albums/list.json", **kwds)["result"] results = self._client.get("/albums/list.json", **kwds)["result"]
return [Album(self._client, album) for album in results] return [Album(self._client, album) for album in results]
class ApiAlbum: class ApiAlbum(object):
def __init__(self, client): def __init__(self, client):
self._client = client self._client = client
@ -30,12 +33,15 @@ class ApiAlbum:
return album.delete(**kwds) return album.delete(**kwds)
def form(self, album, **kwds): def form(self, album, **kwds):
""" Not yet implemented """
raise NotImplementedError() raise NotImplementedError()
def add_photos(self, album, photos, **kwds): def add_photos(self, album, photos, **kwds):
""" Not yet implemented """
raise NotImplementedError() raise NotImplementedError()
def remove_photos(self, album, photos, **kwds): def remove_photos(self, album, photos, **kwds):
""" Not yet implemented """
raise NotImplementedError() raise NotImplementedError()
def update(self, album, **kwds): def update(self, album, **kwds):

View file

@ -1,3 +1,6 @@
"""
api_photo.py : Trovebox Photo API Classes
"""
import base64 import base64
from .errors import TroveboxError from .errors import TroveboxError
@ -17,7 +20,7 @@ def extract_ids(photos):
ids.append(photo) ids.append(photo)
return ids return ids
class ApiPhotos: class ApiPhotos(object):
def __init__(self, client): def __init__(self, client):
self._client = client self._client = client
@ -51,7 +54,7 @@ class ApiPhotos:
raise TroveboxError("Delete response returned False") raise TroveboxError("Delete response returned False")
return True return True
class ApiPhoto: class ApiPhoto(object):
def __init__(self, client): def __init__(self, client):
self._client = client self._client = client
@ -72,9 +75,11 @@ class ApiPhoto:
return photo.edit(**kwds) return photo.edit(**kwds)
def replace(self, photo, photo_file, **kwds): def replace(self, photo, photo_file, **kwds):
""" Not yet implemented """
raise NotImplementedError() raise NotImplementedError()
def replace_encoded(self, photo, photo_file, **kwds): def replace_encoded(self, photo, photo_file, **kwds):
""" Not yet implemented """
raise NotImplementedError() raise NotImplementedError()
def update(self, photo, **kwds): def update(self, photo, **kwds):
@ -114,6 +119,7 @@ class ApiPhoto:
return Photo(self._client, result) return Photo(self._client, result)
def dynamic_url(self, photo, **kwds): def dynamic_url(self, photo, **kwds):
""" Not yet implemented """
raise NotImplementedError() raise NotImplementedError()
def next_previous(self, photo, **kwds): def next_previous(self, photo, **kwds):

View file

@ -1,6 +1,9 @@
"""
api_tag.py : Trovebox Tag API Classes
"""
from .objects import Tag from .objects import Tag
class ApiTags: class ApiTags(object):
def __init__(self, client): def __init__(self, client):
self._client = client self._client = client
@ -9,7 +12,7 @@ class ApiTags:
results = self._client.get("/tags/list.json", **kwds)["result"] results = self._client.get("/tags/list.json", **kwds)["result"]
return [Tag(self._client, tag) for tag in results] return [Tag(self._client, tag) for tag in results]
class ApiTag: class ApiTag(object):
def __init__(self, client): def __init__(self, client):
self._client = client self._client = client

View file

@ -1,3 +1,6 @@
"""
auth.py : OAuth Config File Parser
"""
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
try: try:
@ -9,7 +12,8 @@ try:
except ImportError: except ImportError:
import StringIO as io # Python2 import StringIO as io # Python2
class Config: class Auth(object):
"""OAuth secrets"""
def __init__(self, config_file, host, def __init__(self, config_file, host,
consumer_key, consumer_secret, consumer_key, consumer_secret,
token, token_secret): token, token_secret):
@ -46,7 +50,8 @@ def get_config_path(config_file):
def read_config(config_path): def read_config(config_path):
""" """
Loads config data from the specified file 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" section = "DUMMY"
defaults = {'host': 'localhost', defaults = {'host': 'localhost',

View file

@ -1,3 +1,6 @@
"""
errors.py : Trovebox Error Classes
"""
class TroveboxError(Exception): class TroveboxError(Exception):
""" Indicates that an Trovebox operation failed """ """ Indicates that an Trovebox operation failed """
pass pass

View file

@ -1,16 +1,19 @@
"""
http.py : Trovebox HTTP Access
"""
from __future__ import unicode_literals from __future__ import unicode_literals
import sys import sys
import requests import requests
import requests_oauthlib import requests_oauthlib
import logging import logging
try: try:
from urllib.parse import urlunparse # Python3 from urllib.parse import urlparse, urlunparse # Python3
except ImportError: except ImportError:
from urlparse import urlunparse # Python2 from urlparse import urlparse, urlunparse # Python2
from .objects import TroveboxObject from .objects import TroveboxObject
from .errors import * from .errors import *
from .config import Config from .auth import Auth
if sys.version < '3': if sys.version < '3':
TEXT_TYPE = unicode TEXT_TYPE = unicode
@ -20,36 +23,58 @@ else:
DUPLICATE_RESPONSE = {"code": 409, DUPLICATE_RESPONSE = {"code": 409,
"message": "This photo already exists"} "message": "This photo already exists"}
class Http: class Http(object):
""" """
Base class to handle HTTP requests to an Trovebox server. Base class to handle HTTP requests to an Trovebox server.
If no parameters are specified, config is loaded from the default If no parameters are specified, auth config is loaded from the
location (~/.config/trovebox/default). default location (~/.config/trovebox/default).
The config_file parameter is used to specify an alternate config file. The config_file parameter is used to specify an alternate config file.
If the host parameter is specified, no config file is loaded and If the host parameter is specified, no config file is loaded and
OAuth tokens (consumer*, token*) can optionally be specified. 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, def __init__(self, config_file=None, host=None,
consumer_key='', consumer_secret='', consumer_key='', consumer_secret='',
token='', token_secret='', api_version=None): 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._logger = logging.getLogger("trovebox")
self.config = Config(config_file, host, self.auth = Auth(config_file, host,
consumer_key, consumer_secret, consumer_key, consumer_secret,
token, token_secret) token, token_secret)
self.host = self.config.host self.host = self.auth.host
# Remember the most recent HTTP request and response # Remember the most recent HTTP request and response
self.last_url = None self.last_url = None
self.last_params = None self.last_params = None
self.last_response = 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): def get(self, endpoint, process_response=True, **params):
""" """
Performs an HTTP GET from the specified endpoint (API path), Performs an HTTP GET from the specified endpoint (API path),
@ -62,21 +87,18 @@ class Http:
Returns the raw response if process_response=False Returns the raw response if process_response=False
""" """
params = self._process_params(params) params = self._process_params(params)
if not endpoint.startswith("/"): url = self._construct_url(endpoint)
endpoint = "/" + endpoint
if self._api_version is not None:
endpoint = "/v%d%s" % (self._api_version, endpoint)
url = urlunparse(('http', self.host, endpoint, '', '', ''))
if self.config.consumer_key: if self.auth.consumer_key:
auth = requests_oauthlib.OAuth1(self.config.consumer_key, auth = requests_oauthlib.OAuth1(self.auth.consumer_key,
self.config.consumer_secret, self.auth.consumer_secret,
self.config.token, self.auth.token,
self.config.token_secret) self.auth.token_secret)
else: else:
auth = None auth = None
with requests.Session() as session: with requests.Session() as session:
session.verify = self.config["ssl_verify"]
response = session.get(url, params=params, auth=auth) response = session.get(url, params=params, auth=auth)
self._logger.info("============================") self._logger.info("============================")
@ -105,20 +127,17 @@ class Http:
Returns the raw response if process_response=False Returns the raw response if process_response=False
""" """
params = self._process_params(params) params = self._process_params(params)
if not endpoint.startswith("/"): url = self._construct_url(endpoint)
endpoint = "/" + endpoint
if self._api_version is not None:
endpoint = "/v%d%s" % (self._api_version, endpoint)
url = urlunparse(('http', self.host, endpoint, '', '', ''))
if not self.config.consumer_key: if not self.auth.consumer_key:
raise TroveboxError("Cannot issue POST without OAuth tokens") raise TroveboxError("Cannot issue POST without OAuth tokens")
auth = requests_oauthlib.OAuth1(self.config.consumer_key, auth = requests_oauthlib.OAuth1(self.auth.consumer_key,
self.config.consumer_secret, self.auth.consumer_secret,
self.config.token, self.auth.token,
self.config.token_secret) self.auth.token_secret)
with requests.Session() as session: with requests.Session() as session:
session.verify = self.config["ssl_verify"]
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 = session.post(url, params=params, response = session.post(url, params=params,
@ -146,6 +165,22 @@ class Http:
else: else:
return response.text 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 @staticmethod
def _process_params(params): def _process_params(params):
""" Converts Unicode/lists/booleans inside HTTP parameters """ """ Converts Unicode/lists/booleans inside HTTP parameters """

View file

@ -1,4 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
"""
main.py : Trovebox Console Script
"""
import os import os
import sys import sys
import json import json
@ -44,7 +47,7 @@ def main(args=sys.argv[1:]):
action="store_true", dest="pretty", default=False) action="store_true", dest="pretty", default=False)
parser.add_option('-v', help="Verbose output", parser.add_option('-v', help="Verbose output",
action="store_true", dest="verbose", default=False) 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") action="store_true")
parser.add_option('--help', help='show this help message', parser.add_option('--help', help='show this help message',
action="store_true") action="store_true")
@ -107,7 +110,8 @@ def main(args=sys.argv[1:]):
def extract_files(params): 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: Uses the same technique as the Trovebox PHP commandline tool:
* Filename can only be in the "photo" parameter * Filename can only be in the "photo" parameter
* Filename must be prefixed with "@" * Filename must be prefixed with "@"

View file

@ -1,3 +1,6 @@
"""
objects.py : Basic Trovebox API Objects
"""
try: try:
from urllib.parse import quote # Python3 from urllib.parse import quote # Python3
except ImportError: except ImportError:
@ -5,7 +8,7 @@ except ImportError:
from .errors import TroveboxError from .errors import TroveboxError
class TroveboxObject: class TroveboxObject(object):
""" Base object supporting the storage of custom fields as attributes """ """ Base object supporting the storage of custom fields as attributes """
def __init__(self, trovebox, json_dict): def __init__(self, trovebox, json_dict):
self.id = None self.id = None
@ -75,9 +78,11 @@ class Photo(TroveboxObject):
return result["markup"] return result["markup"]
def replace(self, photo_file, **kwds): def replace(self, photo_file, **kwds):
""" Not implemented yet """
raise NotImplementedError() raise NotImplementedError()
def replace_encoded(self, photo_file, **kwds): def replace_encoded(self, photo_file, **kwds):
""" Not implemented yet """
raise NotImplementedError() raise NotImplementedError()
def update(self, **kwds): def update(self, **kwds):
@ -96,6 +101,7 @@ class Photo(TroveboxObject):
self._replace_fields(new_dict) self._replace_fields(new_dict)
def dynamic_url(self, **kwds): def dynamic_url(self, **kwds):
""" Not implemented yet """
raise NotImplementedError() raise NotImplementedError()
def next_previous(self, **kwds): def next_previous(self, **kwds):
@ -194,12 +200,15 @@ class Album(TroveboxObject):
return result return result
def form(self, **kwds): def form(self, **kwds):
""" Not implemented yet """
raise NotImplementedError() raise NotImplementedError()
def add_photos(self, photos, **kwds): def add_photos(self, photos, **kwds):
""" Not implemented yet """
raise NotImplementedError() raise NotImplementedError()
def remove_photos(self, photos, **kwds): def remove_photos(self, photos, **kwds):
""" Not implemented yet """
raise NotImplementedError() raise NotImplementedError()
def update(self, **kwds): def update(self, **kwds):