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

View file

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

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)
.. 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
================

View file

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

View file

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

View file

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

View file

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

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 .errors import *
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
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):

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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