Merge branch 'release-0.6'

This commit is contained in:
sneakypete81 2013-11-23 13:23:40 +00:00
commit f75f25001d
60 changed files with 2672 additions and 1170 deletions

4
.coveragerc Normal file
View file

@ -0,0 +1,4 @@
# .coveragerc to control coverage.py
[run]
# Capture branch coverage
branch = True

2
.gitignore vendored
View file

@ -6,3 +6,5 @@ dist
tests/tokens.py tests/tokens.py
tests.log tests.log
.tox .tox
.coverage
htmlcov

View file

@ -1,15 +1,23 @@
language: python language: python
install: install:
# Install test dependencies
- pip install tox --use-mirrors - pip install tox --use-mirrors
- .travis/install_pylint - pip install coveralls --use-mirrors
script: tox script: tox
after_success:
# Send coverage results to coveralls.io
- coveralls
after_script: after_script:
# Install dependencies for Pylint # Install dependencies for Pylint
- pip install requests requests-oauthlib - pip install pylint-patcher --use-mirrors
- pip install requests --use-mirrors
- pip install requests-oauthlib --use-mirrors
# Run Pylint # Run Pylint
# Uses pylint-patcher to allow exceptions to be stored in a patchfile
# (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 trovebox - pylint-patcher trovebox

View file

@ -1,10 +0,0 @@
# Until the --use-ignore-patch makes it into pylint upstream, we need to
# download and install from sneakypete81's pylint fork
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-*

View file

@ -2,6 +2,12 @@
Trovebox Python Library Changelog Trovebox Python Library Changelog
================================= =================================
v0.6
======
* Support for many additional API endpoints (#56, #65)
* Code coverage reporting (#57)
* Unit test improvements (#58, #63, #64)
v0.5.1 v0.5.1
====== ======
* Use httpretty v0.6.5 for unit tests (#60) * Use httpretty v0.6.5 for unit tests (#60)

View file

@ -7,6 +7,13 @@ Trovebox Python Library
:alt: Build Status :alt: Build Status
:target: https://travis-ci.org/photo/openphoto-python :target: https://travis-ci.org/photo/openphoto-python
..
(commented out until master is on coveralls.io)
.. image:: https://coveralls.io/repos/photo/openphoto-python/badge.png?branch=master
:alt: Coverage Status
:target: https://coveralls.io/r/photo/openphoto-python?branch=master
..
.. image:: https://pypip.in/v/trovebox/badge.png .. image:: https://pypip.in/v/trovebox/badge.png
:alt: Python Package Index (PyPI) :alt: Python Package Index (PyPI)
:target: https://pypi.python.org/pypi/trovebox :target: https://pypi.python.org/pypi/trovebox

16
pylintrc Normal file
View file

@ -0,0 +1,16 @@
[MESSAGES CONTROL]
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time. See also the "--disable" option for examples.
#enable=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=locally-disabled

View file

@ -9,26 +9,18 @@ tput setaf 3
echo echo
echo "Testing latest self-hosted site..." echo "Testing latest self-hosted site..."
tput sgr0 tput sgr0
sleep 1
export TROVEBOX_TEST_CONFIG=test export TROVEBOX_TEST_CONFIG=test
unset TROVEBOX_TEST_SERVER_API unset TROVEBOX_TEST_SERVER_API
python -m unittest discover --catch tests/functional python -m unittest discover --catch tests/functional
# Test server running APIv1 Trovebox instance
# Install from photo/frontend commit 660b2ab
tput setaf 3
echo
echo "Testing APIv1 self-hosted site..."
tput sgr0
export TROVEBOX_TEST_CONFIG=test-apiv1
export TROVEBOX_TEST_SERVER_API=1
python -m unittest discover --catch tests/functional
# Test server running v3.0.8 Trovebox instance # Test server running v3.0.8 Trovebox instance
# Install from photo/frontend commit e9d81de57b # Install from photo/frontend commit e9d81de57b
tput setaf 3 tput setaf 3
echo echo
echo "Testing v3.0.8 self-hosted site..." echo "Testing v3.0.8 self-hosted site..."
tput sgr0 tput sgr0
sleep 1
export TROVEBOX_TEST_CONFIG=test-3.0.8 export TROVEBOX_TEST_CONFIG=test-3.0.8
unset TROVEBOX_TEST_SERVER_API unset TROVEBOX_TEST_SERVER_API
python -m unittest discover --catch tests/functional python -m unittest discover --catch tests/functional
@ -38,7 +30,18 @@ tput setaf 3
echo echo
echo "Testing latest hosted site..." echo "Testing latest hosted site..."
tput sgr0 tput sgr0
sleep 1
export TROVEBOX_TEST_CONFIG=test-hosted export TROVEBOX_TEST_CONFIG=test-hosted
unset TROVEBOX_TEST_SERVER_API unset TROVEBOX_TEST_SERVER_API
python -m unittest discover --catch tests/functional python -m unittest discover --catch tests/functional
# Test account on hosted trovebox.com site over HTTPS
tput setaf 3
echo
echo "Testing latest hosted site over HTTPS..."
tput sgr0
sleep 1
export TROVEBOX_TEST_CONFIG=test-hosted-https
unset TROVEBOX_TEST_SERVER_API
python -m unittest discover --catch tests/functional

View file

@ -32,8 +32,7 @@ setup(name='trovebox',
long_description=open("README.rst").read(), long_description=open("README.rst").read(),
author='Pete Burgers, James Walker', author='Pete Burgers, James Walker',
url='https://github.com/photo/openphoto-python', url='https://github.com/photo/openphoto-python',
packages=['trovebox'], packages=['trovebox', 'trovebox.objects', 'trovebox.api'],
data_files=['README.rst'],
keywords=['openphoto', 'pyopenphoto', 'openphoto-python', keywords=['openphoto', 'pyopenphoto', 'openphoto-python',
'trovebox', 'pytrovebox', 'trovebox-python'], 'trovebox', 'pytrovebox', 'trovebox-python'],
classifiers=['Development Status :: 4 - Beta', classifiers=['Development Status :: 4 - Beta',

View file

@ -99,7 +99,7 @@ all supported API versions.
To use it, you must set up multiple Trovebox instances and create the following To use it, you must set up multiple Trovebox instances and create the following
config files containing your credentials: config files containing your credentials:
test : Latest self-hosted site (from photo/frontend master branch) test : Latest self-hosted site (from photo/frontend master branch)
test-apiv1 : APIv1 self-hosted site (from photo/frontend commit 660b2ab) test-3.0.8 : v3.0.8 self-hosted site (from photo/frontend commit e9d81de57b)
test-3.0.8 : v3.0.8 self-hosted site (from photo/frontend commit e9d81de57b) test-hosted : Credentials for test account on http://<xxxx>.trovebox.com
test-hosted : Credentials for test account on trovebox.com test-hosted-https : Same as test-hosted, but with https://

View file

@ -0,0 +1,2 @@
# __init__.py

View file

@ -0,0 +1,2 @@
# __init__.py

View file

@ -1,5 +1,12 @@
from tests.functional import test_activities, test_actions
from tests.functional import test_albums, test_photos, test_tags from tests.functional import test_albums, test_photos, test_tags
class TestActivitiesV1(test_activities.TestActivities):
api_version = 1
class TestActionsV1(test_actions.TestActions):
api_version = 1
class TestAlbumsV1(test_albums.TestAlbums): class TestAlbumsV1(test_albums.TestAlbums):
api_version = 1 api_version = 1

View file

@ -2,7 +2,19 @@ try:
import unittest2 as unittest import unittest2 as unittest
except ImportError: except ImportError:
import unittest import unittest
from tests.functional import test_base, test_albums, test_photos, test_tags
from tests.functional import test_base, test_activities, test_actions
from tests.functional import test_albums, test_photos, test_tags
@unittest.skipIf(test_base.get_test_server_api() < 2,
"Don't test future API versions")
class TestActivitiesV2(test_activities.TestActivities):
api_version = 2
@unittest.skipIf(test_base.get_test_server_api() < 2,
"Don't test future API versions")
class TestActionsV2(test_actions.TestActions):
api_version = 2
@unittest.skipIf(test_base.get_test_server_api() < 2, @unittest.skipIf(test_base.get_test_server_api() < 2,
"Don't test future API versions") "Don't test future API versions")

View file

@ -0,0 +1,23 @@
try:
import unittest2 as unittest # Python2.6
except ImportError:
import unittest
import trovebox
from tests.functional import test_base
class TestActions(test_base.TestBase):
testcase_name = "action API"
def test_create_view_delete(self):
""" Create an action on a photo, view it, then delete it """
# Create and check that the action exists
action = self.client.action.create(target=self.photos[0], type="comment", name="test")
action_id = action.id
self.assertEqual(self.client.action.view(action_id).name, "test")
# Delete and check that the action is gone
action.delete()
with self.assertRaises(trovebox.TroveboxError):
self.client.action.view(action_id)

View file

@ -0,0 +1,84 @@
try:
import unittest2 as unittest # Python2.6
except ImportError:
import unittest
from tests.functional import test_base
@unittest.skipIf(test_base.get_test_server_api() == 1,
("Activities never get deleted in v1, which makes "
"these tests too hard to write"))
class TestActivities(test_base.TestBase):
testcase_name = "activity API"
def test_list(self):
"""
Upload three photos, and check that three corresponding activities
are created.
"""
self._delete_all()
self._create_test_photos(tag=False)
photos = self.client.photos.list()
# Check that each activity is for a valid test photo
activities = self.client.activities.list()
self.assertEqual(len(activities), len(photos))
for activity in activities:
self.assertIn(activity.data.id, [photo.id for photo in photos])
# Put the environment back the way we found it
for photo in photos:
photo.update(tags=self.TEST_TAG)
def test_list_option(self):
"""
Check that the activity list options parameter works correctly
"""
self._delete_all()
self._create_test_photos(tag=False)
photos = self.client.photos.list()
# Dummy photo update activity
photos[0].update(tags=photos[0].tags)
# Check that the activities can be filtered
upload_activities = self.client.activities.list(options={"type": "photo-upload"})
update_activities = self.client.activities.list(options={"type": "photo-update"})
self.assertEqual(len(upload_activities), len(photos))
self.assertEqual(len(update_activities), 1)
# Put the environment back the way we found it
for photo in photos:
photo.update(tags=self.TEST_TAG)
# The purge endpoint currently reports a 500: Internal Server Error
# PHP Fatal error:
# Call to undefined method DatabaseMySql::postActivitiesPurge()
# in /var/www/openphoto-master/src/libraries/models/Activity.php
# on line 66
# Tracked in frontend/#1368
@unittest.expectedFailure
def test_purge(self):
""" Test that the purge endpoint deletes all activities """
activities = self.client.activities.list()
self.assertNotEqual(activities, [])
self.client.activities.purge()
activities = self.client.activities.list()
self.assertEqual(activities, [])
def test_view(self):
""" Test that the view endpoint is working correctly """
activity = self.client.activities.list()[0]
fields = activity.get_fields().copy()
# Check that the view method returns the same data as the list
activity.view()
self.assertEqual(fields, activity.get_fields())
# Check using the Trovebox class
activity = self.client.activity.view(activity)
self.assertEqual(fields, activity.get_fields())
# Check passing the activity ID to the Trovebox class
activity = self.client.activity.view(activity.id)
self.assertEqual(fields, activity.get_fields())

View file

@ -1,4 +1,10 @@
try:
import unittest2 as unittest # Python2.6
except ImportError:
import unittest
from tests.functional import test_base from tests.functional import test_base
from trovebox.objects.album import Album
class TestAlbums(test_base.TestBase): class TestAlbums(test_base.TestBase):
testcase_name = "album API" testcase_name = "album API"
@ -53,27 +59,50 @@ class TestAlbums(test_base.TestBase):
self.albums = self.client.albums.list() self.albums = self.client.albums.list()
self.assertEqual(self.albums[0].name, self.TEST_ALBUM) self.assertEqual(self.albums[0].name, self.TEST_ALBUM)
@unittest.skipIf(test_base.get_test_server_api() == 1,
"update_cover was introduced in APIv2")
def test_update_cover(self):
""" Test that an album cover can be updated """
self.albums[0].cover_update(self.photos[0])
self.assertNotEqual(self.albums[0].cover.id, self.photos[1].id)
self.albums[0].cover_update(self.photos[1])
self.assertEqual(self.albums[0].cover.id, self.photos[1].id)
@unittest.skipIf(test_base.get_test_server_api() == 1,
"includeElements was introduced in APIv2")
def test_view(self): def test_view(self):
""" Test the album view """ """ Test the album view """
album = self.albums[0] # Do a view() with includeElements=False, using a fresh Album object
album = Album(self.client, {"id": self.albums[0].id})
album.view()
# Make sure there are no photos reported
self.assertEqual(album.photos, None)
# Get the photos in the album using the Album object directly # Get the photos with includeElements=True
album.view(includeElements=True) album.view(includeElements=True)
# Make sure all photos are in the album # Make sure all photos are in the album
for photo in self.photos: for photo in self.photos:
self.assertIn(photo.id, [p.id for p in album.photos]) self.assertIn(photo.id, [p.id for p in album.photos])
def test_form(self): def test_add_remove(self):
""" If album.form gets implemented, write a test! """ """ Test that photos can be added and removed from an album """
with self.assertRaises(NotImplementedError): # Make sure all photos are in the album
self.client.album.form(None) album = self.albums[0]
album.view(includeElements=True)
for photo in self.photos:
self.assertIn(photo.id, [p.id for p in album.photos])
def test_add_photos(self): # Remove two photos and check that they're gone
""" If album.add_photos gets implemented, write a test! """ album.remove(self.photos[:2])
with self.assertRaises(NotImplementedError): album.view(includeElements=True)
self.client.album.add_photos(None, None) self.assertEqual([p.id for p in album.photos], [self.photos[2].id])
def test_remove_photos(self): # Add a photo and check that it's there
""" If album.remove_photos gets implemented, write a test! """ album.add(self.photos[1])
with self.assertRaises(NotImplementedError): album.view(includeElements=True)
self.client.album.remove_photos(None, None) self.assertNotIn(self.photos[0].id, [p.id for p in album.photos])
self.assertIn(self.photos[1].id, [p.id for p in album.photos])
self.assertIn(self.photos[2].id, [p.id for p in album.photos])
# Put the environment back the way we found it
album.add(self.photos[0])

View file

@ -42,8 +42,8 @@ class TestBase(unittest.TestCase):
else: else:
print("\nTesting %s v%d" % (cls.testcase_name, cls.api_version)) print("\nTesting %s v%d" % (cls.testcase_name, cls.api_version))
cls.client = trovebox.Trovebox(config_file=cls.config_file, cls.client = trovebox.Trovebox(config_file=cls.config_file)
api_version=cls.api_version) cls.client.configure(api_version=cls.api_version)
if cls.client.photos.list() != []: if cls.client.photos.list() != []:
raise ValueError("The test server (%s) contains photos. " raise ValueError("The test server (%s) contains photos. "
@ -124,7 +124,7 @@ class TestBase(unittest.TestCase):
logging.info("Finished %s\n", self.id()) logging.info("Finished %s\n", self.id())
@classmethod @classmethod
def _create_test_photos(cls): def _create_test_photos(cls, tag=True):
""" Upload three test photos """ """ Upload three test photos """
album = cls.client.album.create(cls.TEST_ALBUM) album = cls.client.album.create(cls.TEST_ALBUM)
photos = [ photos = [
@ -139,8 +139,9 @@ class TestBase(unittest.TestCase):
albums=album.id), albums=album.id),
] ]
# Add the test tag, removing any autogenerated tags # Add the test tag, removing any autogenerated tags
for photo in photos: if tag:
photo.update(tags=cls.TEST_TAG) for photo in photos:
photo.update(tags=cls.TEST_TAG)
@classmethod @classmethod
def _delete_all(cls): def _delete_all(cls):

View file

@ -16,8 +16,8 @@ class TestFramework(test_base.TestBase):
""" """
API v0 has a special hello world message API v0 has a special hello world message
""" """
client = trovebox.Trovebox(config_file=self.config_file, client = trovebox.Trovebox(config_file=self.config_file)
api_version=0) client.configure(api_version=0)
result = client.get("hello.json") result = client.get("hello.json")
self.assertEqual(result['message'], self.assertEqual(result['message'],
"Hello, world! This is version zero of the API!") "Hello, world! This is version zero of the API!")
@ -28,8 +28,8 @@ class TestFramework(test_base.TestBase):
For all API versions >0, we get a generic hello world message For all API versions >0, we get a generic hello world message
""" """
for api_version in range(1, test_base.get_test_server_api() + 1): for api_version in range(1, test_base.get_test_server_api() + 1):
client = trovebox.Trovebox(config_file=self.config_file, client = trovebox.Trovebox(config_file=self.config_file)
api_version=api_version) client.configure(api_version=api_version)
result = client.get("hello.json") result = client.get("hello.json")
self.assertEqual(result['message'], "Hello, world!") self.assertEqual(result['message'], "Hello, world!")
self.assertEqual(result['result']['__route__'], self.assertEqual(result['result']['__route__'],
@ -40,8 +40,7 @@ class TestFramework(test_base.TestBase):
If the API version is unspecified, If the API version is unspecified,
we get a generic hello world message. we get a generic hello world message.
""" """
client = trovebox.Trovebox(config_file=self.config_file, client = trovebox.Trovebox(config_file=self.config_file)
api_version=None)
result = client.get("hello.json") result = client.get("hello.json")
self.assertEqual(result['message'], "Hello, world!") self.assertEqual(result['message'], "Hello, world!")
self.assertEqual(result['result']['__route__'], "/hello.json") self.assertEqual(result['result']['__route__'], "/hello.json")
@ -52,7 +51,7 @@ class TestFramework(test_base.TestBase):
(ValueError, since the returned 404 HTML page is not valid JSON) (ValueError, since the returned 404 HTML page is not valid JSON)
""" """
version = trovebox.LATEST_API_VERSION + 1 version = trovebox.LATEST_API_VERSION + 1
client = trovebox.Trovebox(config_file=self.config_file, client = trovebox.Trovebox(config_file=self.config_file)
api_version=version) client.configure(api_version=version)
with self.assertRaises(trovebox.Trovebox404Error): with self.assertRaises(trovebox.Trovebox404Error):
client.get("hello.json") client.get("hello.json")

View file

@ -1,11 +1,39 @@
from __future__ import unicode_literals from __future__ import unicode_literals
try:
import unittest2 as unittest # Python2.6
except ImportError:
import unittest
import requests
import trovebox import trovebox
from tests.functional import test_base from tests.functional import test_base
class TestPhotos(test_base.TestBase): class TestPhotos(test_base.TestBase):
testcase_name = "photo API" testcase_name = "photo API"
def test_list_option(self):
"""
Check that the photo list options parameter works correctly
"""
option_tag = "Filter"
# Assign a photo with a new tag
self.photos[0].update(tagsAdd=option_tag)
# Check that the photos can be filtered
photos = self.client.photos.list(options={"tags": option_tag})
self.assertEqual(len(photos), 1)
self.assertEqual(photos[0].id, self.photos[0].id)
# Put the environment back the way we found it
photos[0].update(tagsRemove=option_tag)
# Photo share endpoint is currently not implemented
@unittest.expectedFailure
def test_share(self):
""" Test photo sharing (currently not implemented) """
self.client.photos.share()
def test_delete_upload(self): def test_delete_upload(self):
""" Test photo deletion and upload """ """ Test photo deletion and upload """
# Delete one photo using the Trovebox class, passing in the id # Delete one photo using the Trovebox class, passing in the id
@ -18,7 +46,7 @@ class TestPhotos(test_base.TestBase):
# Check that they're gone # Check that they're gone
self.assertEqual(self.client.photos.list(), []) self.assertEqual(self.client.photos.list(), [])
# Re-upload the photos, one of them using Bas64 encoding # Re-upload the photos, one of them using Base64 encoding
ret_val = self.client.photo.upload("tests/data/test_photo1.jpg", ret_val = self.client.photo.upload("tests/data/test_photo1.jpg",
title=self.TEST_TITLE) title=self.TEST_TITLE)
self.client.photo.upload("tests/data/test_photo2.jpg", self.client.photo.upload("tests/data/test_photo2.jpg",
@ -47,15 +75,22 @@ class TestPhotos(test_base.TestBase):
self._delete_all() self._delete_all()
self._create_test_photos() self._create_test_photos()
def test_edit(self): def test_delete_source(self):
""" Check that the edit request returns an HTML form """ """ Test that photo source files can be deleted """
# Test using the Trovebox class # Upload a new (duplicate) public photo
html = self.client.photo.edit(self.photos[0]) photo = self.client.photo.upload("tests/data/test_photo1.jpg",
self.assertIn("<form", html.lower()) allowDuplicate=True,
permission=True)
# Check that the photo can be downloaded
self.assertEqual(requests.get(photo.pathOriginal).status_code, 200)
# And the Photo object directly # Delete the source and check that the source file no longer exists
html = self.photos[0].edit() photo.delete_source()
self.assertIn("<form", html.lower()) self.assertIn(requests.get(photo.pathOriginal).status_code,
[403, 404])
# Put the environment back the way we found it
photo.delete()
def test_upload_duplicate(self): def test_upload_duplicate(self):
""" Ensure that duplicate photos are rejected """ """ Ensure that duplicate photos are rejected """
@ -68,6 +103,23 @@ class TestPhotos(test_base.TestBase):
self.photos = self.client.photos.list() self.photos = self.client.photos.list()
self.assertEqual(len(self.photos), 3) self.assertEqual(len(self.photos), 3)
def test_upload_from_url(self):
""" Ensure that a photo can be imported from a URL """
# Make an existing photo public
self.photos[0].update(permission=True)
# Upload a duplicate of an existing photo
self.client.photo.upload_from_url(self.photos[0].pathOriginal,
allowDuplicate=True)
# Check there are now four photos
photos = self.client.photos.list()
self.assertEqual(len(photos), 4)
# Check that the new one is a duplicate
self.assertEqual(photos[0].hash, photos[1].hash)
# Put the environment back the way we found it
photos[1].delete()
self.photos[0].update(permission=False)
def test_update(self): def test_update(self):
""" Update a photo by editing the title """ """ Update a photo by editing the title """
title = "\xfcmlaut" # umlauted umlaut title = "\xfcmlaut" # umlauted umlaut
@ -127,29 +179,29 @@ class TestPhotos(test_base.TestBase):
def test_next_previous(self): def test_next_previous(self):
""" Test the next/previous links of the middle photo """ """ Test the next/previous links of the middle photo """
next_prev = self.client.photo.next_previous(self.photos[1]) next_prev = self.client.photo.next_previous(self.photos[1],
sortBy="dateTaken,asc")
self.assertEqual(next_prev["previous"][0].id, self.photos[0].id) self.assertEqual(next_prev["previous"][0].id, self.photos[0].id)
self.assertEqual(next_prev["next"][0].id, self.photos[2].id) self.assertEqual(next_prev["next"][0].id, self.photos[2].id)
# Do the same using the Photo object directly # Do the same using the Photo object directly
next_prev = self.photos[1].next_previous() next_prev = self.photos[1].next_previous(sortBy="dateTaken,asc")
self.assertEqual(next_prev["previous"][0].id, self.photos[0].id) self.assertEqual(next_prev["previous"][0].id, self.photos[0].id)
self.assertEqual(next_prev["next"][0].id, self.photos[2].id) self.assertEqual(next_prev["next"][0].id, self.photos[2].id)
def test_replace(self): def test_replace(self):
""" If photo.replace gets implemented, write a test! """ """ Test that a photo can be replaced with another """
with self.assertRaises(NotImplementedError): # Replace the first photo with a copy of the second
self.client.photo.replace(None, None) original_hash = self.photos[0].hash
self.assertNotEqual(original_hash, self.photos[1].hash)
def test_replace_encoded(self): self.photos[0].replace("tests/data/test_photo2.jpg",
""" If photo.replace_encoded gets implemented, write a test! """ allowDuplicate=True)
with self.assertRaises(NotImplementedError): # Check that its new hash is correct
self.client.photo.replace_encoded(None, None) self.assertEqual(self.photos[0].hash, self.photos[1].hash)
# Put it back using base64 encoding
def test_dynamic_url(self): self.photos[0].replace_encoded("tests/data/test_photo1.jpg",
""" If photo.dynamic_url gets implemented, write a test! """ allowDuplicate=True)
with self.assertRaises(NotImplementedError): self.assertEqual(self.photos[0].hash, original_hash)
self.client.photo.dynamic_url(None)
def test_transform(self): def test_transform(self):
""" Test photo rotation """ """ Test photo rotation """

View file

@ -0,0 +1,31 @@
import logging
import unittest
import trovebox
from tests.functional import test_base
class TestSystem(test_base.TestBase):
testcase_name = "system"
def setUp(self):
"""
Override the default setUp, since we don't need a populated database
"""
logging.info("\nRunning %s...", self.id())
def test_system_version(self):
"""
Check that the API version string is returned correctly
"""
client = trovebox.Trovebox(config_file=self.config_file)
version = client.system.version()
self.assertEqual(version["api"], "v%s" % trovebox.LATEST_API_VERSION)
@unittest.skip("Diagnostics don't work with the hosted site")
def test_system_diagnostics(self):
"""
Check that the system diagnostics can be performed
"""
client = trovebox.Trovebox(config_file=self.config_file)
diagnostics = client.system.diagnostics()
self.assertIn(diagnostics, "database")

View file

@ -89,7 +89,7 @@ class TestTags(test_base.TestBase):
# TODO: Un-skip this test once issue #919 is resolved - # TODO: Un-skip this test once issue #919 is resolved -
# tags with double-slashes cannot be deleted # tags with double-slashes cannot be deleted
@unittest.expectedFailure @unittest.skip("Tags with double-slashed cannot be deleted")
def test_tag_with_double_slashes(self): def test_tag_with_double_slashes(self):
""" Run test_create_delete using a tag containing double-slashes """ """ Run test_create_delete using a tag containing double-slashes """
self.test_create_delete("tag//with//double//slashes") self.test_create_delete("tag//with//double//slashes")

151
tests/unit/test_actions.py Normal file
View file

@ -0,0 +1,151 @@
from __future__ import unicode_literals
import mock
try:
import unittest2 as unittest # Python2.6
except ImportError:
import unittest
import trovebox
class TestActions(unittest.TestCase):
test_host = "test.example.com"
test_photos_dict = [{"id": "photo1"},
{"id": "photo2"}]
test_actions_dict = [{"id": "1",
"target": test_photos_dict[0],
"target_type": "photo",
"type": "comment",
"totalRows": 2},
{"id": "2",
"target": test_photos_dict[1],
"target_type": "photo",
"type": "comment",
"totalRows": 2}]
def setUp(self):
self.client = trovebox.Trovebox(host=self.test_host)
self.test_photos = [trovebox.objects.photo.Photo(self.client, photo)
for photo in self.test_photos_dict]
self.test_actions = [trovebox.objects.action.Action(self.client, action)
for action in self.test_actions_dict]
@staticmethod
def _return_value(result, message="", code=200):
return {"message": message, "code": code, "result": result}
class TestActionCreate(TestActions):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_action_create(self, mock_post):
"""Check that an action can be created on a photo object"""
mock_post.return_value = self._return_value(self.test_actions_dict[0])
result = self.client.action.create(target=self.test_photos[0], type="comment", foo="bar")
mock_post.assert_called_with("/action/%s/photo/create.json" %
self.test_photos[0].id,
type="comment",
foo="bar")
self.assertEqual(result.id, "1")
self.assertEqual(result.target.id, "photo1")
self.assertEqual(result.target_type, "photo")
self.assertEqual(result.type, "comment")
@mock.patch.object(trovebox.Trovebox, 'post')
def test_action_create_id(self, mock_post):
"""Check that an action can be created using a photo id"""
mock_post.return_value = self._return_value(self.test_actions_dict[0])
result = self.client.action.create(target=self.test_photos[0].id,
target_type="photo", type="comment",
foo="bar")
mock_post.assert_called_with("/action/%s/photo/create.json" %
self.test_photos[0].id,
type="comment",
foo="bar")
self.assertEqual(result.id, "1")
self.assertEqual(result.target.id, "photo1")
self.assertEqual(result.target_type, "photo")
self.assertEqual(result.type, "comment")
@mock.patch.object(trovebox.Trovebox, 'post')
def test_action_create_invalid_type(self, mock_post):
"""
Check that an exception is raised if an action is created on an
invalid object.
"""
with self.assertRaises(AttributeError):
self.client.action.create(target=object())
@mock.patch.object(trovebox.Trovebox, 'post')
def test_action_create_invalid_return_type(self, mock_post):
"""Check that an exception is raised if an invalid object is returned"""
mock_post.return_value = self._return_value({"target": "test",
"target_type": "invalid"})
with self.assertRaises(NotImplementedError):
self.client.action.create(target=self.test_photos[0])
class TestActionDelete(TestActions):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_action_delete(self, mock_post):
"""Check that an action can be deleted"""
mock_post.return_value = self._return_value(True)
result = self.client.action.delete(self.test_actions[0], foo="bar")
mock_post.assert_called_with("/action/1/delete.json", foo="bar")
self.assertEqual(result, True)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_action_delete_id(self, mock_post):
"""Check that an action can be deleted using its ID"""
mock_post.return_value = self._return_value(True)
result = self.client.action.delete("1", foo="bar")
mock_post.assert_called_with("/action/1/delete.json", foo="bar")
self.assertEqual(result, True)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_action_object_delete(self, mock_post):
"""Check that an action can be deleted using the action object directly"""
mock_post.return_value = self._return_value(True)
action = self.test_actions[0]
result = action.delete(foo="bar")
mock_post.assert_called_with("/action/1/delete.json", foo="bar")
self.assertEqual(result, True)
self.assertEqual(action.get_fields(), {})
self.assertEqual(action.id, None)
class TestActionView(TestActions):
@mock.patch.object(trovebox.Trovebox, 'get')
def test_action_view(self, mock_get):
"""Check that an action can be viewed"""
mock_get.return_value = self._return_value(self.test_actions_dict[1])
result = self.client.action.view(self.test_actions[0], name="Test")
mock_get.assert_called_with("/action/1/view.json", name="Test")
self.assertEqual(result.id, "2")
self.assertEqual(result.target.id, "photo2")
self.assertEqual(result.target_type, "photo")
@mock.patch.object(trovebox.Trovebox, 'get')
def test_action_view_id(self, mock_get):
"""Check that an action can be viewed using its ID"""
mock_get.return_value = self._return_value(self.test_actions_dict[1])
result = self.client.action.view("1", name="Test")
mock_get.assert_called_with("/action/1/view.json", name="Test")
self.assertEqual(result.id, "2")
self.assertEqual(result.target.id, "photo2")
self.assertEqual(result.target_type, "photo")
@mock.patch.object(trovebox.Trovebox, 'get')
def test_action_object_view(self, mock_get):
"""Check that an action can be viewed using the action object directly"""
mock_get.return_value = self._return_value(self.test_actions_dict[1])
action = self.test_actions[0]
action.view(name="Test")
mock_get.assert_called_with("/action/1/view.json", name="Test")
self.assertEqual(action.id, "2")
self.assertEqual(action.target.id, "photo2")
self.assertEqual(action.target_type, "photo")
class TestActionMisc(TestActions):
def test_update_fields_with_no_target(self):
"""Check that an action object can be updated with no target"""
action = self.test_actions[0]
action.target = None
action.target_type = None
# Check that no exception is raised
action._update_fields_with_objects()

View file

@ -0,0 +1,142 @@
from __future__ import unicode_literals
import json
import mock
try:
import unittest2 as unittest # Python2.6
except ImportError:
import unittest
import trovebox
class TestActivities(unittest.TestCase):
test_host = "test.example.com"
test_photos_dict = [{"id": "photo1"},
{"id": "photo2"}]
test_activities_dict = [{"id": "1",
"data": test_photos_dict[0],
"type": "photo_upload"},
{"id": "2",
"data": test_photos_dict[1],
"type": "photo_update"}]
def setUp(self):
self.client = trovebox.Trovebox(host=self.test_host)
self.test_photos = [trovebox.objects.photo.Photo(self.client, photo)
for photo in self.test_photos_dict]
self.test_activities = [trovebox.objects.activity.Activity(self.client, activity)
for activity in self.test_activities_dict]
@staticmethod
def _return_value(result, message="", code=200):
return {"message": message, "code": code, "result": result}
@staticmethod
def _view_wrapper(result):
""" The view method returns data enclosed in a dict and JSON encoded """
result["data"] = json.dumps(result["data"])
return {"0": result}
class TestActivitiesList(TestActivities):
@mock.patch.object(trovebox.Trovebox, 'get')
def test_activities_list(self, mock_get):
"""Check that the activity list is returned correctly"""
mock_get.return_value = self._return_value(self.test_activities_dict)
result = self.client.activities.list(foo="bar")
mock_get.assert_called_with("/activities/list.json", foo="bar")
self.assertEqual(len(result), 2)
self.assertEqual(result[0].id, "1")
self.assertEqual(result[0].type, "photo_upload")
self.assertEqual(result[0].data.id, "photo1")
self.assertEqual(result[1].id, "2")
self.assertEqual(result[1].type, "photo_update")
self.assertEqual(result[1].data.id, "photo2")
@mock.patch.object(trovebox.Trovebox, 'get')
def test_empty_result(self, mock_get):
"""Check that an empty result is transformed into an empty list """
mock_get.return_value = self._return_value("")
result = self.client.activities.list(foo="bar")
mock_get.assert_called_with("/activities/list.json", foo="bar")
self.assertEqual(result, [])
@mock.patch.object(trovebox.Trovebox, 'get')
def test_zero_rows(self, mock_get):
"""Check that totalRows=0 is transformed into an empty list """
mock_get.return_value = self._return_value([{"totalRows": 0}])
result = self.client.activities.list(foo="bar")
mock_get.assert_called_with("/activities/list.json", foo="bar")
self.assertEqual(result, [])
@mock.patch.object(trovebox.Trovebox, 'get')
def test_options(self, mock_get):
"""Check that the activity list optionss are applied properly"""
mock_get.return_value = self._return_value(self.test_activities_dict)
self.client.activities.list(options={"foo": "bar",
"test1": "test2"},
foo="bar")
# Dict element can be any order
self.assertIn(mock_get.call_args[0],
[("/activities/foo-bar/test1-test2/list.json",),
("/activities/test1-test2/foo-bar/list.json",)])
self.assertEqual(mock_get.call_args[1], {"foo": "bar"})
class TestActivitiesPurge(TestActivities):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_activity_purge(self, mock_get):
"""Test activity purging """
mock_get.return_value = self._return_value(True)
result = self.client.activities.purge(foo="bar")
mock_get.assert_called_with("/activities/purge.json", foo="bar")
self.assertEqual(result, True)
class TestActivityView(TestActivities):
@mock.patch.object(trovebox.Trovebox, 'get')
def test_activity_view(self, mock_get):
"""Check that a activity can be viewed"""
mock_get.return_value = self._return_value(self._view_wrapper(
self.test_activities_dict[1]))
result = self.client.activity.view(self.test_activities[0],
foo="bar")
mock_get.assert_called_with("/activity/1/view.json", foo="bar")
self.assertEqual(result.get_fields(), self.test_activities_dict[1])
@mock.patch.object(trovebox.Trovebox, 'get')
def test_activity_view_id(self, mock_get):
"""Check that a activity can be viewed using its ID"""
mock_get.return_value = self._return_value(self._view_wrapper(
self.test_activities_dict[1]))
result = self.client.activity.view("1", foo="bar")
mock_get.assert_called_with("/activity/1/view.json", foo="bar")
self.assertEqual(result.get_fields(), self.test_activities_dict[1])
@mock.patch.object(trovebox.Trovebox, 'get')
def test_activity_object_view(self, mock_get):
"""
Check that a activity can be viewed
when using the activity object directly
"""
mock_get.return_value = self._return_value(self._view_wrapper(
self.test_activities_dict[1]))
activity = self.test_activities[0]
activity.view(foo="bar")
mock_get.assert_called_with("/activity/1/view.json", foo="bar")
self.assertEqual(activity.get_fields(), self.test_activities_dict[1])
@mock.patch.object(trovebox.Trovebox, 'get')
def test_activity_view_invalid_type(self, mock_get):
"""Check that an invalid activity type raises an exception"""
mock_get.return_value = self._return_value(self._view_wrapper(
{"data": "", "type": "invalid"}))
with self.assertRaises(NotImplementedError):
self.client.activity.view(self.test_activities[0])
class TestActivityMisc(TestActivities):
def test_update_fields_with_no_type(self):
"""Check that an activity object can be updated with no type"""
activity = self.test_activities[0]
activity.type = None
activity.data = None
# Check that no exception is raised
activity._update_fields_with_objects()

View file

@ -9,17 +9,23 @@ import trovebox
class TestAlbums(unittest.TestCase): class TestAlbums(unittest.TestCase):
test_host = "test.example.com" test_host = "test.example.com"
test_photos_dict = [{"id": "1a", "tags": ["tag1", "tag2"]},
{"id": "2b", "tags": ["tag3", "tag4"]}]
test_albums_dict = [{"cover": {"id": "1a", "tags": ["tag1", "tag2"]}, test_albums_dict = [{"cover": {"id": "1a", "tags": ["tag1", "tag2"]},
"id": "1", "id": "1",
"name": "Album 1", "name": "Album 1",
"photos": [test_photos_dict[0]],
"totalRows": 2}, "totalRows": 2},
{"cover": {"id": "2b", "tags": ["tag3", "tag4"]}, {"cover": {"id": "2b", "tags": ["tag3", "tag4"]},
"id": "2", "id": "2",
"name": "Album 2", "name": "Album 2",
"photos": [test_photos_dict[1]],
"totalRows": 2}] "totalRows": 2}]
def setUp(self): def setUp(self):
self.client = trovebox.Trovebox(host=self.test_host) self.client = trovebox.Trovebox(host=self.test_host)
self.test_albums = [trovebox.objects.Album(self.client, album) self.test_photos = [trovebox.objects.photo.Photo(self.client, photo)
for photo in self.test_photos_dict]
self.test_albums = [trovebox.objects.album.Album(self.client, album)
for album in self.test_albums_dict] for album in self.test_albums_dict]
@staticmethod @staticmethod
@ -31,20 +37,36 @@ class TestAlbumsList(TestAlbums):
def test_albums_list(self, mock_get): def test_albums_list(self, mock_get):
"""Check that the album list is returned correctly""" """Check that the album list is returned correctly"""
mock_get.return_value = self._return_value(self.test_albums_dict) mock_get.return_value = self._return_value(self.test_albums_dict)
result = self.client.albums.list() result = self.client.albums.list(foo="bar")
mock_get.assert_called_with("/albums/list.json") mock_get.assert_called_with("/albums/list.json", foo="bar")
self.assertEqual(len(result), 2) self.assertEqual(len(result), 2)
self.assertEqual(result[0].id, "1") self.assertEqual(result[0].id, "1")
self.assertEqual(result[0].name, "Album 1") self.assertEqual(result[0].name, "Album 1")
self.assertEqual(result[1].id, "2") self.assertEqual(result[1].id, "2")
self.assertEqual(result[1].name, "Album 2") self.assertEqual(result[1].name, "Album 2")
@mock.patch.object(trovebox.Trovebox, 'get')
def test_empty_result(self, mock_get):
"""Check that an empty result is transformed into an empty list """
mock_get.return_value = self._return_value("")
result = self.client.albums.list(foo="bar")
mock_get.assert_called_with("/albums/list.json", foo="bar")
self.assertEqual(result, [])
@mock.patch.object(trovebox.Trovebox, 'get')
def test_zero_rows(self, mock_get):
"""Check that totalRows=0 is transformed into an empty list """
mock_get.return_value = self._return_value([{"totalRows": 0}])
result = self.client.albums.list(foo="bar")
mock_get.assert_called_with("/albums/list.json", foo="bar")
self.assertEqual(result, [])
@mock.patch.object(trovebox.Trovebox, 'get') @mock.patch.object(trovebox.Trovebox, 'get')
def test_albums_list_returns_cover_photos(self, mock_get): def test_albums_list_returns_cover_photos(self, mock_get):
"""Check that the album list returns cover photo objects""" """Check that the album list returns cover photo objects"""
mock_get.return_value = self._return_value(self.test_albums_dict) mock_get.return_value = self._return_value(self.test_albums_dict)
result = self.client.albums.list() result = self.client.albums.list(foo="bar")
mock_get.assert_called_with("/albums/list.json") mock_get.assert_called_with("/albums/list.json", foo="bar")
self.assertEqual(len(result), 2) self.assertEqual(len(result), 2)
self.assertEqual(result[0].id, "1") self.assertEqual(result[0].id, "1")
self.assertEqual(result[0].name, "Album 1") self.assertEqual(result[0].name, "Album 1")
@ -55,6 +77,46 @@ class TestAlbumsList(TestAlbums):
self.assertEqual(result[1].cover.id, "2b") self.assertEqual(result[1].cover.id, "2b")
self.assertEqual(result[1].cover.tags, ["tag3", "tag4"]) self.assertEqual(result[1].cover.tags, ["tag3", "tag4"])
class TestAlbumUpdateCover(TestAlbums):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_album_cover_update(self, mock_post):
"""Check that an album cover can be updated"""
mock_post.return_value = self._return_value(self.test_albums_dict[1])
result = self.client.album.cover_update(self.test_albums[0],
self.test_photos[0],
foo="bar")
mock_post.assert_called_with("/album/1/cover/1a/update.json",
foo="bar")
self.assertEqual(result.id, "2")
self.assertEqual(result.name, "Album 2")
self.assertEqual(result.cover.id, "2b")
self.assertEqual(result.cover.tags, ["tag3", "tag4"])
@mock.patch.object(trovebox.Trovebox, 'post')
def test_album_cover_update_id(self, mock_post):
"""Check that an album cover can be updated using IDs"""
mock_post.return_value = self._return_value(self.test_albums_dict[1])
result = self.client.album.cover_update("1", "1a", foo="bar")
mock_post.assert_called_with("/album/1/cover/1a/update.json",
foo="bar")
self.assertEqual(result.id, "2")
self.assertEqual(result.name, "Album 2")
self.assertEqual(result.cover.id, "2b")
self.assertEqual(result.cover.tags, ["tag3", "tag4"])
@mock.patch.object(trovebox.Trovebox, 'post')
def test_album_object_cover_update(self, mock_post):
"""Check that an album cover can be updated using the album object directly"""
mock_post.return_value = self._return_value(self.test_albums_dict[1])
album = self.test_albums[0]
album.cover_update(self.test_photos[1], foo="bar")
mock_post.assert_called_with("/album/1/cover/2b/update.json",
foo="bar")
self.assertEqual(album.id, "2")
self.assertEqual(album.name, "Album 2")
self.assertEqual(album.cover.id, "2b")
self.assertEqual(album.cover.tags, ["tag3", "tag4"])
class TestAlbumCreate(TestAlbums): class TestAlbumCreate(TestAlbums):
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_album_create(self, mock_post): def test_album_create(self, mock_post):
@ -73,104 +135,153 @@ class TestAlbumDelete(TestAlbums):
def test_album_delete(self, mock_post): def test_album_delete(self, mock_post):
"""Check that an album can be deleted""" """Check that an album can be deleted"""
mock_post.return_value = self._return_value(True) mock_post.return_value = self._return_value(True)
result = self.client.album.delete(self.test_albums[0]) result = self.client.album.delete(self.test_albums[0], foo="bar")
mock_post.assert_called_with("/album/1/delete.json") mock_post.assert_called_with("/album/1/delete.json", foo="bar")
self.assertEqual(result, True) self.assertEqual(result, True)
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_album_delete_id(self, mock_post): def test_album_delete_id(self, mock_post):
"""Check that an album can be deleted using its ID""" """Check that an album can be deleted using its ID"""
mock_post.return_value = self._return_value(True) mock_post.return_value = self._return_value(True)
result = self.client.album.delete("1") result = self.client.album.delete("1", foo="bar")
mock_post.assert_called_with("/album/1/delete.json") mock_post.assert_called_with("/album/1/delete.json", foo="bar")
self.assertEqual(result, True) self.assertEqual(result, True)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_album_delete_failure(self, mock_post):
"""Check that an exception is raised if an album cannot be deleted"""
mock_post.return_value = self._return_value(False)
with self.assertRaises(trovebox.TroveboxError):
self.client.album.delete(self.test_albums[0])
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_album_object_delete(self, mock_post): def test_album_object_delete(self, mock_post):
"""Check that an album can be deleted using the album object directly""" """Check that an album can be deleted using the album object directly"""
mock_post.return_value = self._return_value(True) mock_post.return_value = self._return_value(True)
album = self.test_albums[0] album = self.test_albums[0]
result = album.delete() result = album.delete(foo="bar")
mock_post.assert_called_with("/album/1/delete.json") mock_post.assert_called_with("/album/1/delete.json", foo="bar")
self.assertEqual(result, True) self.assertEqual(result, True)
self.assertEqual(album.get_fields(), {}) self.assertEqual(album.get_fields(), {})
self.assertEqual(album.id, None) self.assertEqual(album.id, None)
self.assertEqual(album.name, None) self.assertEqual(album.name, None)
class TestAlbumAdd(TestAlbums):
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_album_object_delete_failure(self, mock_post): def test_album_add(self, mock_post):
""" Check that photos can be added to an album """
mock_post.return_value = self._return_value(self.test_albums_dict[1])
result = self.client.album.add(self.test_albums[0], self.test_photos,
foo="bar")
mock_post.assert_called_with("/album/1/photo/add.json",
ids=["1a", "2b"], foo="bar")
self.assertEqual(result.id, self.test_albums[1].id)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_album_add_id(self, mock_post):
""" Check that photos can be added to an album using IDs """
mock_post.return_value = self._return_value(self.test_albums_dict[1])
result = self.client.album.add(self.test_albums[0].id,
objects=["1a", "2b"],
object_type="photo",
foo="bar")
mock_post.assert_called_with("/album/1/photo/add.json",
ids=["1a", "2b"], foo="bar")
self.assertEqual(result.id, self.test_albums[1].id)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_album_object_add(self, mock_post):
""" """
Check that an exception is raised if an album cannot be deleted Check that photos can be added to an album using the
when using the album object directly album object directly
""" """
mock_post.return_value = self._return_value(False) mock_post.return_value = self._return_value(self.test_albums_dict[1])
with self.assertRaises(trovebox.TroveboxError): album = self.test_albums[0]
self.test_albums[0].delete() album.add(self.test_photos, foo="bar")
mock_post.assert_called_with("/album/1/photo/add.json",
class TestAlbumForm(TestAlbums): ids=["1a", "2b"], foo="bar")
@mock.patch.object(trovebox.Trovebox, 'post') self.assertEqual(album.id, self.test_albums[1].id)
def test_album_form(self, _):
""" If album.form gets implemented, write a test! """
with self.assertRaises(NotImplementedError):
self.client.album.form(self.test_albums[0])
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_album_form_id(self, _): def test_album_add_single(self, mock_post):
""" If album.form gets implemented, write a test! """ """ Check that a single photo can be added to an album """
with self.assertRaises(NotImplementedError): mock_post.return_value = self._return_value(self.test_albums_dict[1])
self.client.album.form("1") self.test_albums[0].add(self.test_photos[0], foo="bar")
mock_post.assert_called_with("/album/1/photo/add.json",
ids=["1a"], foo="bar")
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_album_object_form(self, _): def test_album_add_invalid_type(self, _):
""" If album.form gets implemented, write a test! """ """
with self.assertRaises(NotImplementedError): Check that an exception is raised if an invalid object is added
self.test_albums[0].form() to an album.
"""
class TestAlbumAddPhotos(TestAlbums): with self.assertRaises(AttributeError):
@mock.patch.object(trovebox.Trovebox, 'post') self.test_albums[0].add([object()])
def test_album_add_photos(self, _):
""" If album.add_photos gets implemented, write a test! """
with self.assertRaises(NotImplementedError):
self.client.album.add_photos(self.test_albums[0], ["Photo Objects"])
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_album_add_photos_id(self, _): def test_album_add_multiple_types(self, _):
""" If album.add_photos gets implemented, write a test! """ """
with self.assertRaises(NotImplementedError): Check that an exception is raised if multiple types are added
self.client.album.add_photos("1", ["Photo Objects"]) to an album.
"""
@mock.patch.object(trovebox.Trovebox, 'post') with self.assertRaises(ValueError):
def test_album_object_add_photos(self, _): self.test_albums[0].add(self.test_photos+self.test_albums)
""" If album.add_photos gets implemented, write a test! """
with self.assertRaises(NotImplementedError):
self.test_albums[0].add_photos(["Photo Objects"])
class TestAlbumRemovePhotos(TestAlbums): class TestAlbumRemovePhotos(TestAlbums):
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_album_remove_photos(self, _): def test_album_remove(self, mock_post):
""" If album.remove_photos gets implemented, write a test! """ """ Check that photos can be removed from an album """
with self.assertRaises(NotImplementedError): mock_post.return_value = self._return_value(self.test_albums_dict[1])
self.client.album.remove_photos(self.test_albums[0], result = self.client.album.remove(self.test_albums[0], self.test_photos,
["Photo Objects"]) foo="bar")
mock_post.assert_called_with("/album/1/photo/remove.json",
ids=["1a", "2b"], foo="bar")
self.assertEqual(result.id, self.test_albums[1].id)
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_album_remove_photos_id(self, _): def test_album_remove_id(self, mock_post):
""" If album.remove_photos gets implemented, write a test! """ """ Check that photos can be removed from an album using IDs """
with self.assertRaises(NotImplementedError): mock_post.return_value = self._return_value(self.test_albums_dict[1])
self.client.album.remove_photos("1", ["Photo Objects"]) result = self.client.album.remove(self.test_albums[0].id,
objects=["1a", "2b"],
object_type="photo",
foo="bar")
mock_post.assert_called_with("/album/1/photo/remove.json",
ids=["1a", "2b"], foo="bar")
self.assertEqual(result.id, self.test_albums[1].id)
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_album_object_remove_photos(self, _): def test_album_object_remove(self, mock_post):
""" If album.remove_photos gets implemented, write a test! """ """
with self.assertRaises(NotImplementedError): Check that photos can be removed from an album using the
self.test_albums[0].remove_photos(["Photo Objects"]) album object directly
"""
mock_post.return_value = self._return_value(self.test_albums_dict[1])
album = self.test_albums[0]
album.remove(self.test_photos, foo="bar")
mock_post.assert_called_with("/album/1/photo/remove.json",
ids=["1a", "2b"], foo="bar")
self.assertEqual(album.id, self.test_albums[1].id)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_album_remove_single(self, mock_post):
""" Check that a single photo can be removed from an album """
mock_post.return_value = self._return_value(self.test_albums_dict[1])
self.test_albums[0].remove(self.test_photos[0], foo="bar")
mock_post.assert_called_with("/album/1/photo/remove.json",
ids=["1a"], foo="bar")
@mock.patch.object(trovebox.Trovebox, 'post')
def test_album_remove_invalid_type(self, _):
"""
Check that an exception is raised if an invalid object is removed
from an album.
"""
with self.assertRaises(AttributeError):
self.test_albums[0].remove([object()])
@mock.patch.object(trovebox.Trovebox, 'post')
def test_album_remove_multiple_types(self, _):
"""
Check that an exception is raised if multiple types are removed
from an album.
"""
with self.assertRaises(ValueError):
self.test_albums[0].remove(self.test_photos+self.test_albums)
class TestAlbumUpdate(TestAlbums): class TestAlbumUpdate(TestAlbums):
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
@ -212,33 +323,44 @@ class TestAlbumView(TestAlbums):
def test_album_view(self, mock_get): def test_album_view(self, mock_get):
"""Check that an album can be viewed""" """Check that an album can be viewed"""
mock_get.return_value = self._return_value(self.test_albums_dict[1]) mock_get.return_value = self._return_value(self.test_albums_dict[1])
result = self.client.album.view(self.test_albums[0], name="Test") result = self.client.album.view(self.test_albums[0], includeElements=True)
mock_get.assert_called_with("/album/1/view.json", name="Test") mock_get.assert_called_with("/album/1/view.json", includeElements=True)
self.assertEqual(result.id, "2") self.assertEqual(result.id, "2")
self.assertEqual(result.name, "Album 2") self.assertEqual(result.name, "Album 2")
self.assertEqual(result.cover.id, "2b") self.assertEqual(result.cover.id, "2b")
self.assertEqual(result.cover.tags, ["tag3", "tag4"]) self.assertEqual(result.cover.tags, ["tag3", "tag4"])
self.assertEqual(result.photos[0].id, self.test_photos[1].id)
@mock.patch.object(trovebox.Trovebox, 'get') @mock.patch.object(trovebox.Trovebox, 'get')
def test_album_view_id(self, mock_get): def test_album_view_id(self, mock_get):
"""Check that an album can be viewed using its ID""" """Check that an album can be viewed using its ID"""
mock_get.return_value = self._return_value(self.test_albums_dict[1]) mock_get.return_value = self._return_value(self.test_albums_dict[1])
result = self.client.album.view("1", name="Test") result = self.client.album.view("1", includeElements=True)
mock_get.assert_called_with("/album/1/view.json", name="Test") mock_get.assert_called_with("/album/1/view.json", includeElements=True)
self.assertEqual(result.id, "2") self.assertEqual(result.id, "2")
self.assertEqual(result.name, "Album 2") self.assertEqual(result.name, "Album 2")
self.assertEqual(result.cover.id, "2b") self.assertEqual(result.cover.id, "2b")
self.assertEqual(result.cover.tags, ["tag3", "tag4"]) self.assertEqual(result.cover.tags, ["tag3", "tag4"])
self.assertEqual(result.photos[0].id, self.test_photos[1].id)
@mock.patch.object(trovebox.Trovebox, 'get') @mock.patch.object(trovebox.Trovebox, 'get')
def test_album_object_view(self, mock_get): def test_album_object_view(self, mock_get):
"""Check that an album can be viewed using the album object directly""" """Check that an album can be viewed using the album object directly"""
mock_get.return_value = self._return_value(self.test_albums_dict[1]) mock_get.return_value = self._return_value(self.test_albums_dict[1])
album = self.test_albums[0] album = self.test_albums[0]
album.view(name="Test") album.view(includeElements=True)
mock_get.assert_called_with("/album/1/view.json", name="Test") mock_get.assert_called_with("/album/1/view.json", includeElements=True)
self.assertEqual(album.id, "2") self.assertEqual(album.id, "2")
self.assertEqual(album.name, "Album 2") self.assertEqual(album.name, "Album 2")
self.assertEqual(album.cover.id, "2b") self.assertEqual(album.cover.id, "2b")
self.assertEqual(album.cover.tags, ["tag3", "tag4"]) self.assertEqual(album.cover.tags, ["tag3", "tag4"])
self.assertEqual(album.photos[0].id, self.test_photos[1].id)
class TestAlbumMisc(TestAlbums):
def test_update_fields_with_no_cover(self):
"""Check that an album object can be updated with no cover"""
album = self.test_albums[0]
album.cover = None
album.photos = None
# Check that no exception is raised
album._update_fields_with_objects()

View file

@ -31,7 +31,8 @@ class TestAuth(unittest.TestCase):
def create_config(config_file, host): def create_config(config_file, host):
"""Create a dummy config file""" """Create a dummy config file"""
with open(os.path.join(CONFIG_PATH, config_file), "w") as conf: with open(os.path.join(CONFIG_PATH, config_file), "w") as conf:
conf.write("host = %s\n" % host) if host is not None:
conf.write("host = %s\n" % host)
conf.write("# Comment\n\n") conf.write("# Comment\n\n")
conf.write("consumerKey = \"%s_consumer_key\"\n" % config_file) conf.write("consumerKey = \"%s_consumer_key\"\n" % config_file)
conf.write("\"consumerSecret\"= %s_consumer_secret\n" % config_file) conf.write("\"consumerSecret\"= %s_consumer_secret\n" % config_file)
@ -97,4 +98,13 @@ class TestAuth(unittest.TestCase):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
Trovebox(config_file="custom", host="host_override") Trovebox(config_file="custom", host="host_override")
def test_partial_config_file(self):
""" Test that an incomplete config file causes default values to be set """
self.create_config("incomplete", host=None) # Don't write the host line
client = Trovebox(config_file="incomplete")
auth = client.auth
self.assertEqual(auth.host, "localhost")
self.assertEqual(auth.consumer_key, "incomplete_consumer_key")
self.assertEqual(auth.consumer_secret, "incomplete_consumer_secret")
self.assertEqual(auth.token, "incomplete_token")
self.assertEqual(auth.token_secret, "incomplete_token_secret")

View file

@ -106,11 +106,21 @@ class TestCli(unittest.TestCase):
@mock.patch.object(trovebox.main.trovebox, "Trovebox") @mock.patch.object(trovebox.main.trovebox, "Trovebox")
@mock.patch('sys.stdout', new_callable=io.StringIO) @mock.patch('sys.stdout', new_callable=io.StringIO)
def test_verbose(self, mock_stdout, _): def test_verbose_without_params(self, mock_stdout, _):
"""Check that the verbose option is working""" """Check that the verbose option works with no parameters"""
main(["-v"]) main(["-v"])
self.assertIn("Method: GET", mock_stdout.getvalue()) self.assertIn("Method: GET", mock_stdout.getvalue())
self.assertIn("Endpoint: /photos/list.json", mock_stdout.getvalue()) self.assertIn("Endpoint: /photos/list.json", mock_stdout.getvalue())
self.assertNotIn("Fields:", mock_stdout.getvalue())
@mock.patch.object(trovebox.main.trovebox, "Trovebox")
@mock.patch('sys.stdout', new_callable=io.StringIO)
def test_verbose_with_params(self, mock_stdout, _):
"""Check that the verbose option works with parameters"""
main(["-v", "-F foo=bar"])
self.assertIn("Method: GET", mock_stdout.getvalue())
self.assertIn("Endpoint: /photos/list.json", mock_stdout.getvalue())
self.assertIn("Fields:\n foo=bar", mock_stdout.getvalue())
@mock.patch.object(trovebox.main.trovebox, "Trovebox") @mock.patch.object(trovebox.main.trovebox, "Trovebox")
@mock.patch('sys.stdout', new_callable=io.StringIO) @mock.patch('sys.stdout', new_callable=io.StringIO)
@ -127,3 +137,8 @@ class TestCli(unittest.TestCase):
main(["--version"]) main(["--version"])
self.assertEqual(mock_stdout.getvalue(), trovebox.__version__ + "\n") self.assertEqual(mock_stdout.getvalue(), trovebox.__version__ + "\n")
@mock.patch('sys.stdout', new_callable=io.StringIO)
def test_help(self, mock_stdout):
"""Check that the help string is correctly printed"""
main(["--help"])
self.assertIn("show this help message", mock_stdout.getvalue())

View file

@ -112,6 +112,22 @@ class TestHttp(unittest.TestCase):
"https://test.example.com/%s" % self.test_endpoint) "https://test.example.com/%s" % self.test_endpoint)
self.assertEqual(self.client.last_response.json(), self.test_data) self.assertEqual(self.client.last_response.json(), self.test_data)
@httpretty.activate
@data(GET, POST)
def test_endpoint_leading_slash(self, method):
"""Check that an endpoint with a leading slash is constructed correctly"""
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 @httpretty.activate
def test_get_with_parameters(self): def test_get_with_parameters(self):
"""Check that the get method accepts parameters correctly""" """Check that the get method accepts parameters correctly"""
@ -171,12 +187,13 @@ class TestHttp(unittest.TestCase):
def test_get_parameter_processing(self): def test_get_parameter_processing(self):
"""Check that the parameter processing function is working""" """Check that the parameter processing function is working"""
self._register_uri(httpretty.GET) self._register_uri(httpretty.GET)
photo = trovebox.objects.Photo(None, {"id": "photo_id"}) photo = trovebox.objects.photo.Photo(None, {"id": "photo_id"})
album = trovebox.objects.Album(None, {"id": "album_id"}) album = trovebox.objects.album.Album(None, {"id": "album_id"})
tag = trovebox.objects.Tag(None, {"id": "tag_id"}) tag = trovebox.objects.tag.Tag(None, {"id": "tag_id"})
self.client.get(self.test_endpoint, self.client.get(self.test_endpoint,
photo=photo, album=album, tag=tag, photo=photo, album=album, tag=tag,
list_=[photo, album, tag], list_=[photo, album, tag],
list2=["1", "2", "3"],
boolean=True, boolean=True,
unicode_="\xfcmlaut") unicode_="\xfcmlaut")
params = self._last_request().querystring params = self._last_request().querystring
@ -184,6 +201,7 @@ class TestHttp(unittest.TestCase):
self.assertEqual(params["album"], ["album_id"]) self.assertEqual(params["album"], ["album_id"])
self.assertEqual(params["tag"], ["tag_id"]) self.assertEqual(params["tag"], ["tag_id"])
self.assertEqual(params["list_"], ["photo_id,album_id,tag_id"]) self.assertEqual(params["list_"], ["photo_id,album_id,tag_id"])
self.assertEqual(params["list2"], ["1,2,3"])
self.assertEqual(params["boolean"], ["1"]) self.assertEqual(params["boolean"], ["1"])
self.assertIn(params["unicode_"], [["\xc3\xbcmlaut"], ["\xfcmlaut"]]) self.assertIn(params["unicode_"], [["\xc3\xbcmlaut"], ["\xfcmlaut"]])

View file

@ -1,14 +1,18 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import json import json
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:
import unittest import unittest
from test_http import GetOrPost
import trovebox import trovebox
@ddt
class TestHttpErrors(unittest.TestCase): class TestHttpErrors(unittest.TestCase):
test_host = "test.example.com" test_host = "test.example.com"
test_endpoint = "test.json" test_endpoint = "test.json"
@ -38,146 +42,93 @@ class TestHttpErrors(unittest.TestCase):
**kwds) **kwds)
@httpretty.activate @httpretty.activate
def test_get_with_error_status(self): @data(GET, POST)
def test_error_status(self, method):
""" """
Check that an error status causes the get method Check that an error status causes the get/post methods
to raise an exception to raise an exception
""" """
self._register_uri(httpretty.GET, status=500) self._register_uri(method, status=500)
with self.assertRaises(trovebox.TroveboxError): with self.assertRaises(trovebox.TroveboxError):
self.client.get(self.test_endpoint) GetOrPost(self.client, method).call(self.test_endpoint)
@httpretty.activate @httpretty.activate
def test_post_with_error_status(self): @data(GET, POST)
def test_404_status(self, method):
""" """
Check that an error status causes the post method Check that a 404 status causes the get/post methods
to raise an exception
"""
self._register_uri(httpretty.POST, status=500)
with self.assertRaises(trovebox.TroveboxError):
self.client.post(self.test_endpoint)
@httpretty.activate
def test_get_with_404_status(self):
"""
Check that a 404 status causes the get method
to raise a 404 exception to raise a 404 exception
""" """
self._register_uri(httpretty.GET, status=404) self._register_uri(method, status=404)
with self.assertRaises(trovebox.Trovebox404Error): with self.assertRaises(trovebox.Trovebox404Error):
self.client.get(self.test_endpoint) GetOrPost(self.client, method).call(self.test_endpoint)
@httpretty.activate @httpretty.activate
def test_post_with_404_status(self): @data(GET, POST)
def test_with_invalid_json(self, method):
""" """
Check that a 404 status causes the post method Check that invalid JSON causes the get/post methods to
to raise a 404 exception
"""
self._register_uri(httpretty.POST, status=404)
with self.assertRaises(trovebox.Trovebox404Error):
self.client.post(self.test_endpoint)
@httpretty.activate
def test_get_with_invalid_json(self):
"""
Check that invalid JSON causes the get method to
raise an exception raise an exception
""" """
self._register_uri(httpretty.GET, body="Invalid JSON") self._register_uri(method, body="Invalid JSON")
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
self.client.get(self.test_endpoint) GetOrPost(self.client, method).call(self.test_endpoint)
@httpretty.activate @httpretty.activate
def test_post_with_invalid_json(self): @data(GET, POST)
def test_with_error_status_and_invalid_json(self, method):
""" """
Check that invalid JSON causes the post method to Check that invalid JSON causes the get/post methods to raise
raise an exception an exception, even with an error status is returned
""" """
self._register_uri(httpretty.POST, body="Invalid JSON") self._register_uri(method, body="Invalid JSON", status=500)
with self.assertRaises(ValueError):
self.client.post(self.test_endpoint)
@httpretty.activate
def test_get_with_error_status_and_invalid_json(self):
"""
Check that invalid JSON causes the get method to raise an exception,
even with an error status is returned
"""
self._register_uri(httpretty.GET, body="Invalid JSON", status=500)
with self.assertRaises(trovebox.TroveboxError): with self.assertRaises(trovebox.TroveboxError):
self.client.get(self.test_endpoint) GetOrPost(self.client, method).call(self.test_endpoint)
@httpretty.activate @httpretty.activate
def test_post_with_error_status_and_invalid_json(self): @data(GET, POST)
def test_with_404_status_and_invalid_json(self, method):
""" """
Check that invalid JSON causes the post method to raise an exception, Check that invalid JSON causes the get/post methods to raise
even with an error status is returned an exception, even with a 404 status is returned
""" """
self._register_uri(httpretty.POST, body="Invalid JSON", status=500) self._register_uri(method, body="Invalid JSON", status=404)
with self.assertRaises(trovebox.TroveboxError):
self.client.post(self.test_endpoint)
@httpretty.activate
def test_get_with_404_status_and_invalid_json(self):
"""
Check that invalid JSON causes the get method to raise an exception,
even with a 404 status is returned
"""
self._register_uri(httpretty.GET, body="Invalid JSON", status=404)
with self.assertRaises(trovebox.Trovebox404Error): with self.assertRaises(trovebox.Trovebox404Error):
self.client.get(self.test_endpoint) GetOrPost(self.client, method).call(self.test_endpoint)
@httpretty.activate @httpretty.activate
def test_post_with_404_status_and_invalid_json(self): @data(GET, POST)
def test_with_duplicate_status(self, method):
""" """
Check that invalid JSON causes the post method to raise an exception, Check that a get/post with a duplicate status
even with a 404 status is returned
"""
self._register_uri(httpretty.POST, body="Invalid JSON", status=404)
with self.assertRaises(trovebox.Trovebox404Error):
self.client.post(self.test_endpoint)
@httpretty.activate
def test_get_with_duplicate_status(self):
"""
Check that a get with a duplicate status
raises a duplicate exception raises a duplicate exception
""" """
data = {"message": "This photo already exists", "code": 409} data = {"message": "This photo already exists", "code": 409}
self._register_uri(httpretty.GET, data=data, status=409) self._register_uri(method, data=data, status=409)
with self.assertRaises(trovebox.TroveboxDuplicateError): with self.assertRaises(trovebox.TroveboxDuplicateError):
self.client.get(self.test_endpoint) GetOrPost(self.client, method).call(self.test_endpoint)
@httpretty.activate @httpretty.activate
def test_post_with_duplicate_status(self): @data(GET, POST)
""" def test_with_status_code_mismatch(self, method):
Check that a post with a duplicate status
raises a duplicate exception
"""
data = {"message": "This photo already exists", "code": 409}
self._register_uri(httpretty.POST, data=data, status=409)
with self.assertRaises(trovebox.TroveboxDuplicateError):
self.client.post(self.test_endpoint)
@httpretty.activate
def test_get_with_status_code_mismatch(self):
""" """
Check that a mismatched HTTP status code still returns the Check that a mismatched HTTP status code still returns the
JSON status code for get requests. JSON status code.
""" """
data = {"message": "Test Message", "code": 202} data = {"message": "Test Message", "code": 202}
self._register_uri(httpretty.GET, data=data, status=200) self._register_uri(method, data=data, status=200)
response = self.client.get(self.test_endpoint) response = GetOrPost(self.client, method).call(self.test_endpoint)
self.assertEqual(response["code"], 202) self.assertEqual(response["code"], 202)
@httpretty.activate @httpretty.activate
def test_post_with_status_code_mismatch(self): @data(GET, POST)
def test_http_error_with_no_response_processing(self, method):
""" """
Check that a mismatched HTTP status code still returns the Check that get/post methods work with response processing disabled
JSON status code for post requests. when an HTTP error code is returned.
""" """
data = {"message": "Test Message", "code": 202} httpretty.register_uri(method, self.test_uri, status=500)
self._register_uri(httpretty.POST, data=data, status=200) with self.assertRaises(trovebox.TroveboxError):
response = self.client.post(self.test_endpoint) response = GetOrPost(self.client, method).call(self.test_endpoint,
self.assertEqual(response["code"], 202) process_response=False)

View file

@ -18,7 +18,7 @@ class TestPhotos(unittest.TestCase):
"totalPages": 1, "totalRows": 2}] "totalPages": 1, "totalRows": 2}]
def setUp(self): def setUp(self):
self.client = trovebox.Trovebox(host=self.test_host) self.client = trovebox.Trovebox(host=self.test_host)
self.test_photos = [trovebox.objects.Photo(self.client, photo) self.test_photos = [trovebox.objects.photo.Photo(self.client, photo)
for photo in self.test_photos_dict] for photo in self.test_photos_dict]
@staticmethod @staticmethod
@ -31,14 +31,55 @@ class TestPhotosList(TestPhotos):
"""Check that the photo list is returned correctly""" """Check that the photo list is returned correctly"""
mock_get.return_value = self._return_value(self.test_photos_dict) mock_get.return_value = self._return_value(self.test_photos_dict)
result = self.client.photos.list() result = self.client.photos.list(foo="bar")
mock_get.assert_called_with("/photos/list.json") mock_get.assert_called_with("/photos/list.json", foo="bar")
self.assertEqual(len(result), 2) self.assertEqual(len(result), 2)
self.assertEqual(result[0].id, "1a") self.assertEqual(result[0].id, "1a")
self.assertEqual(result[0].tags, ["tag1", "tag2"]) self.assertEqual(result[0].tags, ["tag1", "tag2"])
self.assertEqual(result[1].id, "2b") self.assertEqual(result[1].id, "2b")
self.assertEqual(result[1].tags, ["tag3", "tag4"]) self.assertEqual(result[1].tags, ["tag3", "tag4"])
@mock.patch.object(trovebox.Trovebox, 'get')
def test_empty_result(self, mock_get):
"""Check that an empty result is transformed into an empty list """
mock_get.return_value = self._return_value("")
result = self.client.photos.list(foo="bar")
mock_get.assert_called_with("/photos/list.json", foo="bar")
self.assertEqual(result, [])
@mock.patch.object(trovebox.Trovebox, 'get')
def test_zero_rows(self, mock_get):
"""Check that totalRows=0 is transformed into an empty list """
mock_get.return_value = self._return_value([{"totalRows": 0}])
result = self.client.photos.list(foo="bar")
mock_get.assert_called_with("/photos/list.json", foo="bar")
self.assertEqual(result, [])
@mock.patch.object(trovebox.Trovebox, 'get')
def test_options(self, mock_get):
"""Check that the activity list options are applied properly"""
mock_get.return_value = self._return_value(self.test_photos_dict)
self.client.photos.list(options={"foo": "bar",
"test1": "test2"},
foo="bar")
# Dict element can be any order
self.assertIn(mock_get.call_args[0],
[("/photos/foo-bar/test1-test2/list.json",),
("/photos/test1-test2/foo-bar/list.json",)])
self.assertEqual(mock_get.call_args[1], {"foo": "bar"})
class TestPhotosShare(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photos_share(self, mock_post):
self.client.photos.share(options={"foo": "bar",
"test1": "test2"},
foo="bar")
# Dict element can be any order
self.assertIn(mock_post.call_args[0],
[("/photos/foo-bar/test1-test2/share.json",),
("/photos/test1-test2/foo-bar/share.json",)])
self.assertEqual(mock_post.call_args[1], {"foo": "bar"})
class TestPhotosUpdate(TestPhotos): class TestPhotosUpdate(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_photos_update(self, mock_post): def test_photos_update(self, mock_post):
@ -58,67 +99,42 @@ class TestPhotosUpdate(TestPhotos):
ids=["1a", "2b"], title="Test") ids=["1a", "2b"], title="Test")
self.assertEqual(result, True) self.assertEqual(result, True)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photos_update_failure(self, mock_post):
"""
Check that an exception is raised if multiple photos
cannot be updated
"""
mock_post.return_value = self._return_value(False)
with self.assertRaises(trovebox.TroveboxError):
self.client.photos.update(self.test_photos, title="Test")
class TestPhotosDelete(TestPhotos): class TestPhotosDelete(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_photos_delete(self, mock_post): def test_photos_delete(self, mock_post):
"""Check that multiple photos can be deleted""" """Check that multiple photos can be deleted"""
mock_post.return_value = self._return_value(True) mock_post.return_value = self._return_value(True)
result = self.client.photos.delete(self.test_photos) result = self.client.photos.delete(self.test_photos, foo="bar")
mock_post.assert_called_with("/photos/delete.json", ids=["1a", "2b"]) mock_post.assert_called_with("/photos/delete.json",
ids=["1a", "2b"], foo="bar")
self.assertEqual(result, True) self.assertEqual(result, True)
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_photos_delete_ids(self, mock_post): def test_photos_delete_ids(self, mock_post):
"""Check that multiple photos can be deleted using their IDs""" """Check that multiple photos can be deleted using their IDs"""
mock_post.return_value = self._return_value(True) mock_post.return_value = self._return_value(True)
result = self.client.photos.delete(["1a", "2b"]) result = self.client.photos.delete(["1a", "2b"], foo="bar")
mock_post.assert_called_with("/photos/delete.json", ids=["1a", "2b"]) mock_post.assert_called_with("/photos/delete.json",
ids=["1a", "2b"], foo="bar")
self.assertEqual(result, True) self.assertEqual(result, True)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photos_delete_failure(self, mock_post):
"""
Check that an exception is raised if multiple photos
cannot be deleted
"""
mock_post.return_value = self._return_value(False)
with self.assertRaises(trovebox.TroveboxError):
self.client.photos.delete(self.test_photos)
class TestPhotoDelete(TestPhotos): class TestPhotoDelete(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_delete(self, mock_post): def test_photo_delete(self, mock_post):
"""Check that a photo can be deleted""" """Check that a photo can be deleted"""
mock_post.return_value = self._return_value(True) mock_post.return_value = self._return_value(True)
result = self.client.photo.delete(self.test_photos[0]) result = self.client.photo.delete(self.test_photos[0], foo="bar")
mock_post.assert_called_with("/photo/1a/delete.json") mock_post.assert_called_with("/photo/1a/delete.json", foo="bar")
self.assertEqual(result, True) self.assertEqual(result, True)
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_delete_id(self, mock_post): def test_photo_delete_id(self, mock_post):
"""Check that a photo can be deleted using its ID""" """Check that a photo can be deleted using its ID"""
mock_post.return_value = self._return_value(True) mock_post.return_value = self._return_value(True)
result = self.client.photo.delete("1a") result = self.client.photo.delete("1a", foo="bar")
mock_post.assert_called_with("/photo/1a/delete.json") mock_post.assert_called_with("/photo/1a/delete.json", foo="bar")
self.assertEqual(result, True) self.assertEqual(result, True)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_delete_failure(self, mock_post):
"""Check that an exception is raised if a photo cannot be deleted"""
mock_post.return_value = self._return_value(False)
with self.assertRaises(trovebox.TroveboxError):
self.client.photo.delete(self.test_photos[0])
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_object_delete(self, mock_post): def test_photo_object_delete(self, mock_post):
""" """
@ -127,87 +143,189 @@ class TestPhotoDelete(TestPhotos):
""" """
mock_post.return_value = self._return_value(True) mock_post.return_value = self._return_value(True)
photo = self.test_photos[0] photo = self.test_photos[0]
result = photo.delete() result = photo.delete(foo="bar")
mock_post.assert_called_with("/photo/1a/delete.json") mock_post.assert_called_with("/photo/1a/delete.json", foo="bar")
self.assertEqual(result, True) self.assertEqual(result, True)
self.assertEqual(photo.get_fields(), {}) self.assertEqual(photo.get_fields(), {})
self.assertEqual(photo.id, None) self.assertEqual(photo.id, None)
class TestPhotoDeleteSource(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_object_delete_failure(self, mock_post): def test_photo_delete_source(self, mock_post):
""" """Check that photo source files can be deleted"""
Check that an exception is raised if a photo cannot be deleted mock_post.return_value = self._return_value(True)
when using the photo object directly result = self.client.photo.delete_source(self.test_photos[0], foo="bar")
""" mock_post.assert_called_with("/photo/1a/source/delete.json", foo="bar")
mock_post.return_value = self._return_value(False) self.assertEqual(result, True)
with self.assertRaises(trovebox.TroveboxError):
self.test_photos[0].delete()
class TestPhotoEdit(TestPhotos): @mock.patch.object(trovebox.Trovebox, 'post')
@mock.patch.object(trovebox.Trovebox, 'get') def test_photo_delete_source_id(self, mock_post):
def test_photo_edit(self, mock_get): """Check that photo source files can be deleted using its ID"""
"""Check that a the photo edit endpoint is working""" mock_post.return_value = self._return_value(True)
mock_get.return_value = self._return_value({"markup": "<form/>"}) result = self.client.photo.delete_source("1a", foo="bar")
result = self.client.photo.edit(self.test_photos[0]) mock_post.assert_called_with("/photo/1a/source/delete.json", foo="bar")
mock_get.assert_called_with("/photo/1a/edit.json") self.assertEqual(result, True)
self.assertEqual(result, "<form/>")
@mock.patch.object(trovebox.Trovebox, 'get') @mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_edit_id(self, mock_get): def test_photo_object_delete_source(self, mock_post):
"""Check that a the photo edit endpoint is working when using an ID"""
mock_get.return_value = self._return_value({"markup": "<form/>"})
result = self.client.photo.edit("1a")
mock_get.assert_called_with("/photo/1a/edit.json")
self.assertEqual(result, "<form/>")
@mock.patch.object(trovebox.Trovebox, 'get')
def test_photo_object_edit(self, mock_get):
""" """
Check that a the photo edit endpoint is working Check that photo source files can be deleted when using
when using the photo object directly the photo object directly
""" """
mock_get.return_value = self._return_value({"markup": "<form/>"}) mock_post.return_value = self._return_value(True)
result = self.test_photos[0].edit() photo = self.test_photos[0]
mock_get.assert_called_with("/photo/1a/edit.json") result = photo.delete_source(foo="bar")
self.assertEqual(result, "<form/>") mock_post.assert_called_with("/photo/1a/source/delete.json", foo="bar")
self.assertEqual(result, True)
class TestPhotoReplace(TestPhotos): class TestPhotoReplace(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_replace(self, _): def test_photo_replace(self, mock_post):
""" If photo.replace gets implemented, write a test! """ """Check that an existing photo can be replaced"""
with self.assertRaises(NotImplementedError): mock_post.return_value = self._return_value(self.test_photos_dict[0])
self.client.photo.replace(self.test_photos[0], self.test_file) result = self.client.photo.replace(self.test_photos[1],
self.test_file, title="Test")
# It's not possible to compare the file object,
# so check each parameter individually
endpoint = mock_post.call_args[0]
title = mock_post.call_args[1]["title"]
files = mock_post.call_args[1]["files"]
self.assertEqual(endpoint,
("/photo/%s/replace.json" % self.test_photos[1].id,))
self.assertEqual(title, "Test")
self.assertIn("photo", files)
self.assertEqual(result.get_fields(), self.test_photos_dict[0])
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_replace_id(self, _): def test_photo_replace_id(self, mock_post):
""" If photo.replace gets implemented, write a test! """ """Check that an existing photo can be replaced using its ID"""
with self.assertRaises(NotImplementedError): mock_post.return_value = self._return_value(self.test_photos_dict[0])
self.client.photo.replace("1a", self.test_file) result = self.client.photo.replace(self.test_photos[1].id,
self.test_file, title="Test")
# It's not possible to compare the file object,
# so check each parameter individually
endpoint = mock_post.call_args[0]
title = mock_post.call_args[1]["title"]
files = mock_post.call_args[1]["files"]
self.assertEqual(endpoint,
("/photo/%s/replace.json" % self.test_photos[1].id,))
self.assertEqual(title, "Test")
self.assertIn("photo", files)
self.assertEqual(result.get_fields(), self.test_photos_dict[0])
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_object_replace(self, _): def test_photo_object_replace(self, mock_post):
""" If photo.replace gets implemented, write a test! """ """
with self.assertRaises(NotImplementedError): Check that an existing photo can be replaced when using the
self.test_photos[0].replace(self.test_file) Photo object directly.
"""
photo_id = self.test_photos[1].id
mock_post.return_value = self._return_value(self.test_photos_dict[0])
self.test_photos[1].replace(self.test_file, title="Test")
# It's not possible to compare the file object,
# so check each parameter individually
endpoint = mock_post.call_args[0]
title = mock_post.call_args[1]["title"]
files = mock_post.call_args[1]["files"]
self.assertEqual(endpoint, ("/photo/%s/replace.json" % photo_id,))
self.assertEqual(title, "Test")
self.assertIn("photo", files)
self.assertEqual(self.test_photos[1].get_fields(),
self.test_photos_dict[0])
class TestPhotoReplaceEncoded(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_replace_encoded(self, mock_post):
"""
Check that a photo can be uploaded using Base64 encoding to
replace an existing photo.
"""
mock_post.return_value = self._return_value(self.test_photos_dict[0])
result = self.client.photo.replace_encoded(self.test_photos[1],
self.test_file, title="Test")
with open(self.test_file, "rb") as in_file:
encoded_file = base64.b64encode(in_file.read())
mock_post.assert_called_with("/photo/%s/replace.json"
% self.test_photos[1].id,
photo=encoded_file, title="Test")
self.assertEqual(result.get_fields(), self.test_photos_dict[0])
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_replace_encoded(self, _): def test_photo_replace_encoded_id(self, mock_post):
""" If photo.replace_encoded gets implemented, write a test! """ """
with self.assertRaises(NotImplementedError): Check that a photo can be uploaded using Base64 encoding to
self.client.photo.replace_encoded(self.test_photos[0], replace an existing photo using its ID.
self.test_file) """
mock_post.return_value = self._return_value(self.test_photos_dict[0])
result = self.client.photo.replace_encoded(self.test_photos[1].id,
self.test_file, title="Test")
with open(self.test_file, "rb") as in_file:
encoded_file = base64.b64encode(in_file.read())
mock_post.assert_called_with("/photo/%s/replace.json"
% self.test_photos[1].id,
photo=encoded_file, title="Test")
self.assertEqual(result.get_fields(), self.test_photos_dict[0])
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_replace_encoded_id(self, _): def test_photo_object_replace_encoded(self, mock_post):
""" If photo.replace_encoded gets implemented, write a test! """ """
with self.assertRaises(NotImplementedError): Check that a photo can be uploaded using Base64 encoding to
self.client.photo.replace_encoded("1a", self.test_file) replace an existing photo when using the Photo object directly.
"""
photo_id = self.test_photos[1].id
mock_post.return_value = self._return_value(self.test_photos_dict[0])
self.test_photos[1].replace_encoded(self.test_file, title="Test")
with open(self.test_file, "rb") as in_file:
encoded_file = base64.b64encode(in_file.read())
mock_post.assert_called_with("/photo/%s/replace.json"
% photo_id,
photo=encoded_file, title="Test")
self.assertEqual(self.test_photos[1].get_fields(),
self.test_photos_dict[0])
class TestPhotoReplaceFromUrl(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_replace_from_url(self, mock_post):
"""
Check that a photo can be imported from a url to
replace an existing photo.
"""
mock_post.return_value = self._return_value(self.test_photos_dict[0])
result = self.client.photo.replace_from_url(self.test_photos[1],
"test_url", title="Test")
mock_post.assert_called_with("/photo/%s/replace.json"
% self.test_photos[1].id,
photo="test_url", title="Test")
self.assertEqual(result.get_fields(), self.test_photos_dict[0])
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_object_replace_encoded(self, _): def test_photo_id_replace_from_url(self, mock_post):
""" If photo.replace_encoded gets implemented, write a test! """ """
with self.assertRaises(NotImplementedError): Check that a photo can be imported from a url to
self.test_photos[0].replace_encoded(photo_file=self.test_file) replace an existing photo using its ID.
"""
mock_post.return_value = self._return_value(self.test_photos_dict[0])
result = self.client.photo.replace_from_url(self.test_photos[1].id,
"test_url", title="Test")
mock_post.assert_called_with("/photo/%s/replace.json"
% self.test_photos[1].id,
photo="test_url", title="Test")
self.assertEqual(result.get_fields(), self.test_photos_dict[0])
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_object_replace_from_url(self, mock_post):
"""
Check that a photo can be imported from a url to
replace an existing photo when using the Photo object directly.
"""
photo_id = self.test_photos[1].id
mock_post.return_value = self._return_value(self.test_photos_dict[0])
self.test_photos[1].replace_from_url("test_url", title="Test")
mock_post.assert_called_with("/photo/%s/replace.json"
% photo_id,
photo="test_url", title="Test")
self.assertEqual(self.test_photos[1].get_fields(),
self.test_photos_dict[0])
class TestPhotoUpdate(TestPhotos): class TestPhotoUpdate(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
@ -244,16 +362,30 @@ class TestPhotoView(TestPhotos):
"""Check that a photo can be viewed""" """Check that a photo can be viewed"""
mock_get.return_value = self._return_value(self.test_photos_dict[1]) mock_get.return_value = self._return_value(self.test_photos_dict[1])
result = self.client.photo.view(self.test_photos[0], result = self.client.photo.view(self.test_photos[0],
options={"foo": "bar",
"test1": "test2"},
returnSizes="20x20") returnSizes="20x20")
mock_get.assert_called_with("/photo/1a/view.json", returnSizes="20x20") # Dict elemet can be in any order
self.assertIn(mock_get.call_args[0],
[("/photo/1a/foo-bar/test1-test2/view.json",),
("/photo/1a/test1-test2/foo-bar/view.json",)])
self.assertEqual(mock_get.call_args[1], {"returnSizes": "20x20"})
self.assertEqual(result.get_fields(), self.test_photos_dict[1]) self.assertEqual(result.get_fields(), self.test_photos_dict[1])
@mock.patch.object(trovebox.Trovebox, 'get') @mock.patch.object(trovebox.Trovebox, 'get')
def test_photo_view_id(self, mock_get): def test_photo_view_id(self, mock_get):
"""Check that a photo can be viewed using its ID""" """Check that a photo can be viewed using its ID"""
mock_get.return_value = self._return_value(self.test_photos_dict[1]) mock_get.return_value = self._return_value(self.test_photos_dict[1])
result = self.client.photo.view("1a", returnSizes="20x20") result = self.client.photo.view("1a",
mock_get.assert_called_with("/photo/1a/view.json", returnSizes="20x20") options={"foo": "bar",
"test1": "test2"},
returnSizes="20x20")
# Dict elemet can be in any order
self.assertIn(mock_get.call_args[0],
[("/photo/1a/foo-bar/test1-test2/view.json",),
("/photo/1a/test1-test2/foo-bar/view.json",)])
self.assertEqual(mock_get.call_args[1], {"returnSizes": "20x20"})
self.assertEqual(result.get_fields(), self.test_photos_dict[1]) self.assertEqual(result.get_fields(), self.test_photos_dict[1])
@mock.patch.object(trovebox.Trovebox, 'get') @mock.patch.object(trovebox.Trovebox, 'get')
@ -264,8 +396,14 @@ class TestPhotoView(TestPhotos):
""" """
mock_get.return_value = self._return_value(self.test_photos_dict[1]) mock_get.return_value = self._return_value(self.test_photos_dict[1])
photo = self.test_photos[0] photo = self.test_photos[0]
photo.view(returnSizes="20x20") photo.view(returnSizes="20x20", options={"foo": "bar",
mock_get.assert_called_with("/photo/1a/view.json", returnSizes="20x20") "test1": "test2"})
# Dict elemet can be in any order
self.assertIn(mock_get.call_args[0],
[("/photo/1a/foo-bar/test1-test2/view.json",),
("/photo/1a/test1-test2/foo-bar/view.json",)])
self.assertEqual(mock_get.call_args[1], {"returnSizes": "20x20"})
self.assertEqual(photo.get_fields(), self.test_photos_dict[1]) self.assertEqual(photo.get_fields(), self.test_photos_dict[1])
class TestPhotoUpload(TestPhotos): class TestPhotoUpload(TestPhotos):
@ -284,6 +422,7 @@ class TestPhotoUpload(TestPhotos):
self.assertIn("photo", files) self.assertIn("photo", files)
self.assertEqual(result.get_fields(), self.test_photos_dict[0]) self.assertEqual(result.get_fields(), self.test_photos_dict[0])
class TestPhotoUploadEncoded(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_upload_encoded(self, mock_post): def test_photo_upload_encoded(self, mock_post):
"""Check that a photo can be uploaded using Base64 encoding""" """Check that a photo can be uploaded using Base64 encoding"""
@ -295,24 +434,17 @@ class TestPhotoUpload(TestPhotos):
photo=encoded_file, title="Test") photo=encoded_file, title="Test")
self.assertEqual(result.get_fields(), self.test_photos_dict[0]) self.assertEqual(result.get_fields(), self.test_photos_dict[0])
class TestPhotoDynamicUrl(TestPhotos): class TestPhotoUploadFromUrl(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'get') @mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_dynamic_url(self, _): def test_photo_upload_from_url(self, mock_post):
""" If photo.dynamic_url gets implemented, write a test! """ """
with self.assertRaises(NotImplementedError): Check that a photo can be imported from a url.
self.client.photo.dynamic_url(self.test_photos[0]) """
mock_post.return_value = self._return_value(self.test_photos_dict[0])
@mock.patch.object(trovebox.Trovebox, 'get') result = self.client.photo.upload_from_url("test_url", title="Test")
def test_photo_dynamic_url_id(self, _): mock_post.assert_called_with("/photo/upload.json",
""" If photo.dynamic_url gets implemented, write a test! """ photo="test_url", title="Test")
with self.assertRaises(NotImplementedError): self.assertEqual(result.get_fields(), self.test_photos_dict[0])
self.client.photo.dynamic_url("1a")
@mock.patch.object(trovebox.Trovebox, 'get')
def test_photo_object_dynamic_url(self, _):
""" If photo.dynamic_url gets implemented, write a test! """
with self.assertRaises(NotImplementedError):
self.test_photos[0].dynamic_url()
class TestPhotoNextPrevious(TestPhotos): class TestPhotoNextPrevious(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'get') @mock.patch.object(trovebox.Trovebox, 'get')
@ -321,8 +453,15 @@ class TestPhotoNextPrevious(TestPhotos):
mock_get.return_value = self._return_value( mock_get.return_value = self._return_value(
{"next": [self.test_photos_dict[0]], {"next": [self.test_photos_dict[0]],
"previous": [self.test_photos_dict[1]]}) "previous": [self.test_photos_dict[1]]})
result = self.client.photo.next_previous(self.test_photos[0]) result = self.client.photo.next_previous(self.test_photos[0],
mock_get.assert_called_with("/photo/1a/nextprevious.json") options={"foo": "bar",
"test1": "test2"},
foo="bar")
# Dict elemet can be in any order
self.assertIn(mock_get.call_args[0],
[("/photo/1a/nextprevious/foo-bar/test1-test2.json",),
("/photo/1a/nextprevious/test1-test2/foo-bar.json",)])
self.assertEqual(mock_get.call_args[1], {"foo": "bar"})
self.assertEqual(result["next"][0].get_fields(), self.assertEqual(result["next"][0].get_fields(),
self.test_photos_dict[0]) self.test_photos_dict[0])
self.assertEqual(result["previous"][0].get_fields(), self.assertEqual(result["previous"][0].get_fields(),
@ -337,8 +476,15 @@ class TestPhotoNextPrevious(TestPhotos):
mock_get.return_value = self._return_value( mock_get.return_value = self._return_value(
{"next": [self.test_photos_dict[0]], {"next": [self.test_photos_dict[0]],
"previous": [self.test_photos_dict[1]]}) "previous": [self.test_photos_dict[1]]})
result = self.client.photo.next_previous("1a") result = self.client.photo.next_previous("1a",
mock_get.assert_called_with("/photo/1a/nextprevious.json") options={"foo": "bar",
"test1": "test2"},
foo="bar")
# Dict elemet can be in any order
self.assertIn(mock_get.call_args[0],
[("/photo/1a/nextprevious/foo-bar/test1-test2.json",),
("/photo/1a/nextprevious/test1-test2/foo-bar.json",)])
self.assertEqual(mock_get.call_args[1], {"foo": "bar"})
self.assertEqual(result["next"][0].get_fields(), self.assertEqual(result["next"][0].get_fields(),
self.test_photos_dict[0]) self.test_photos_dict[0])
self.assertEqual(result["previous"][0].get_fields(), self.assertEqual(result["previous"][0].get_fields(),
@ -353,8 +499,14 @@ class TestPhotoNextPrevious(TestPhotos):
mock_get.return_value = self._return_value( mock_get.return_value = self._return_value(
{"next": [self.test_photos_dict[0]], {"next": [self.test_photos_dict[0]],
"previous": [self.test_photos_dict[1]]}) "previous": [self.test_photos_dict[1]]})
result = self.test_photos[0].next_previous() result = self.test_photos[0].next_previous(options={"foo": "bar",
mock_get.assert_called_with("/photo/1a/nextprevious.json") "test1": "test2"},
foo="bar")
# Dict elemet can be in any order
self.assertIn(mock_get.call_args[0],
[("/photo/1a/nextprevious/foo-bar/test1-test2.json",),
("/photo/1a/nextprevious/test1-test2/foo-bar.json",)])
self.assertEqual(mock_get.call_args[1], {"foo": "bar"})
self.assertEqual(result["next"][0].get_fields(), self.assertEqual(result["next"][0].get_fields(),
self.test_photos_dict[0]) self.test_photos_dict[0])
self.assertEqual(result["previous"][0].get_fields(), self.assertEqual(result["previous"][0].get_fields(),
@ -365,8 +517,10 @@ class TestPhotoNextPrevious(TestPhotos):
"""Check that the next photos are returned""" """Check that the next photos are returned"""
mock_get.return_value = self._return_value( mock_get.return_value = self._return_value(
{"next": [self.test_photos_dict[0]]}) {"next": [self.test_photos_dict[0]]})
result = self.client.photo.next_previous(self.test_photos[0]) result = self.client.photo.next_previous(self.test_photos[0],
mock_get.assert_called_with("/photo/1a/nextprevious.json") foo="bar")
mock_get.assert_called_with("/photo/1a/nextprevious.json",
foo="bar")
self.assertEqual(result["next"][0].get_fields(), self.assertEqual(result["next"][0].get_fields(),
self.test_photos_dict[0]) self.test_photos_dict[0])
self.assertNotIn("previous", result) self.assertNotIn("previous", result)
@ -376,8 +530,10 @@ class TestPhotoNextPrevious(TestPhotos):
"""Check that the previous photos are returned""" """Check that the previous photos are returned"""
mock_get.return_value = self._return_value( mock_get.return_value = self._return_value(
{"previous": [self.test_photos_dict[1]]}) {"previous": [self.test_photos_dict[1]]})
result = self.client.photo.next_previous(self.test_photos[0]) result = self.client.photo.next_previous(self.test_photos[0],
mock_get.assert_called_with("/photo/1a/nextprevious.json") foo="bar")
mock_get.assert_called_with("/photo/1a/nextprevious.json",
foo="bar")
self.assertEqual(result["previous"][0].get_fields(), self.assertEqual(result["previous"][0].get_fields(),
self.test_photos_dict[1]) self.test_photos_dict[1])
self.assertNotIn("next", result) self.assertNotIn("next", result)
@ -388,8 +544,10 @@ class TestPhotoNextPrevious(TestPhotos):
mock_get.return_value = self._return_value( mock_get.return_value = self._return_value(
{"next": [self.test_photos_dict[0], self.test_photos_dict[0]], {"next": [self.test_photos_dict[0], self.test_photos_dict[0]],
"previous": [self.test_photos_dict[1], self.test_photos_dict[1]]}) "previous": [self.test_photos_dict[1], self.test_photos_dict[1]]})
result = self.client.photo.next_previous(self.test_photos[0]) result = self.client.photo.next_previous(self.test_photos[0],
mock_get.assert_called_with("/photo/1a/nextprevious.json") foo="bar")
mock_get.assert_called_with("/photo/1a/nextprevious.json",
foo="bar")
self.assertEqual(result["next"][0].get_fields(), self.assertEqual(result["next"][0].get_fields(),
self.test_photos_dict[0]) self.test_photos_dict[0])
self.assertEqual(result["next"][1].get_fields(), self.assertEqual(result["next"][1].get_fields(),
@ -427,3 +585,96 @@ class TestPhotoTransform(TestPhotos):
photo.transform(rotate="90") photo.transform(rotate="90")
mock_post.assert_called_with("/photo/1a/transform.json", rotate="90") mock_post.assert_called_with("/photo/1a/transform.json", rotate="90")
self.assertEqual(photo.get_fields(), self.test_photos_dict[1]) self.assertEqual(photo.get_fields(), self.test_photos_dict[1])
class TestPhotoObject(TestPhotos):
def test_photo_object_repr_without_id_or_name(self):
"""
Ensure the string representation on an object includes its class name
if the ID and Name attributes don't exist.
"""
photo = trovebox.objects.photo.Photo(self.client, {})
self.assertEqual(repr(photo), "<Photo>")
def test_photo_object_repr_with_id(self):
""" Ensure the string representation on an object includes its id, if present """
photo = trovebox.objects.photo.Photo(self.client, {"id": "Test ID"})
self.assertEqual(repr(photo), "<Photo id='Test ID'>")
def test_photo_object_repr_with_id_and_name(self):
""" Ensure the string representation on an object includes its name, if present """
photo = trovebox.objects.photo.Photo(self.client, {"id": "Test ID",
"name": "Test Name"})
self.assertEqual(repr(photo), "<Photo name='Test Name'>")
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_object_create_attribute(self, _):
"""
Check that attributes are created when creating a
Photo object
"""
photo = trovebox.objects.photo.Photo(self.client, {"attribute": "test"})
self.assertEqual(photo.attribute, "test")
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_object_delete_attribute(self, _):
"""
Check that attributes are deleted when creating a
Photo object
"""
photo = trovebox.objects.photo.Photo(self.client, {"attribute": "test"})
photo.delete()
with self.assertRaises(AttributeError):
value = photo.attribute
self.assertEqual(photo.get_fields(), {})
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_object_update_attribute(self, mock_post):
"""
Check that attributes are updated when creating a
Photo object
"""
photo = trovebox.objects.photo.Photo(self.client, {"attribute": "test"})
mock_post.return_value = self._return_value({"attribute": "test2"})
photo.update()
self.assertEqual(photo.attribute, "test2")
self.assertEqual(photo.get_fields(), {"attribute": "test2"})
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_object_create_illegal_attribute(self, _):
"""
Check that illegal attributes are ignored when creating a
Photo object
"""
photo = trovebox.objects.photo.Photo(self.client, {"_illegal_attribute": "test"})
# The object's attribute shouldn't be created
with self.assertRaises(AttributeError):
value = photo._illegal_attribute
# The field dict gets created correctly, however.
self.assertEqual(photo.get_fields(), {"_illegal_attribute": "test"})
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_object_delete_illegal_attribute(self, _):
"""
Check that illegal attributes are ignored when deleting a
Photo object
"""
photo = trovebox.objects.photo.Photo(self.client, {"_illegal_attribute": "test"})
photo.delete()
with self.assertRaises(AttributeError):
value = photo._illegal_attribute
self.assertEqual(photo.get_fields(), {})
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_object_update_illegal_attribute(self, mock_post):
"""
Check that illegal attributes are ignored when updating a
Photo object
"""
photo = trovebox.objects.photo.Photo(self.client, {"_illegal_attribute": "test"})
mock_post.return_value = self._return_value({"_illegal_attribute": "test2"})
photo.update()
# The object's attribute shouldn't be created
with self.assertRaises(AttributeError):
value = photo._illegal_attribute
# The field dict gets updated correctly, however.
self.assertEqual(photo.get_fields(), {"_illegal_attribute": "test2"})

66
tests/unit/test_system.py Normal file
View file

@ -0,0 +1,66 @@
from __future__ import unicode_literals
import json
import httpretty
from httpretty import GET
try:
import unittest2 as unittest # Python2.6
except ImportError:
import unittest
import trovebox
class TestSystem(unittest.TestCase):
test_host = "test.example.com"
def setUp(self):
self.client = trovebox.Trovebox(host=self.test_host)
@staticmethod
def _return_value(result, message="", code=200):
return json.dumps({"message": message, "code": code, "result": result})
class TestSystemVersion(TestSystem):
test_result = {"api": "v2",
"database": "2.0.0"}
@httpretty.activate
def test_version(self):
"""Check that the version dictionary is returned correctly"""
httpretty.register_uri(GET, uri="http://test.example.com/system/version.json",
body=self._return_value(self.test_result),
status=200)
response = self.client.system.version()
self.assertEqual(response, self.test_result)
class TestSystemDiagnostics(TestSystem):
test_result = {'database': [{'label': 'failure',
'message': 'Could not properly connect to the database.',
'status': False}],
}
@httpretty.activate
def test_diagnostics_pass(self):
"""Check that the diagnostics dictionary is returned correctly on success"""
httpretty.register_uri(GET, uri="http://test.example.com/system/diagnostics.json",
body=self._return_value(self.test_result),
status=200)
response = self.client.system.diagnostics()
self.assertEqual(response, self.test_result)
@httpretty.activate
def test_diagnostics_fail(self):
"""
Check that the diagnostics dictionary is returned correctly on failure.
Although the JSON code is 500, no exception should be raised.
"""
# On failure, the diagnostics endpoint returns a JSON code of 500
# and a response status code of 200.
httpretty.register_uri(GET, uri="http://test.example.com/system/diagnostics.json",
body=self._return_value(self.test_result, code=500),
status=200)
response = self.client.system.diagnostics()
self.assertEqual(response, self.test_result)

View file

@ -15,7 +15,7 @@ class TestTags(unittest.TestCase):
def setUp(self): def setUp(self):
self.client = trovebox.Trovebox(host=self.test_host) self.client = trovebox.Trovebox(host=self.test_host)
self.test_tags = [trovebox.objects.Tag(self.client, tag) self.test_tags = [trovebox.objects.tag.Tag(self.client, tag)
for tag in self.test_tags_dict] for tag in self.test_tags_dict]
@staticmethod @staticmethod
@ -25,61 +25,70 @@ class TestTags(unittest.TestCase):
class TestTagsList(TestTags): class TestTagsList(TestTags):
@mock.patch.object(trovebox.Trovebox, 'get') @mock.patch.object(trovebox.Trovebox, 'get')
def test_tags_list(self, mock_get): def test_tags_list(self, mock_get):
"""Check that the the tag list is returned correctly""" """Check that the tag list is returned correctly"""
mock_get.return_value = self._return_value(self.test_tags_dict) mock_get.return_value = self._return_value(self.test_tags_dict)
result = self.client.tags.list() result = self.client.tags.list(foo="bar")
mock_get.assert_called_with("/tags/list.json") mock_get.assert_called_with("/tags/list.json", foo="bar")
self.assertEqual(len(result), 2) self.assertEqual(len(result), 2)
self.assertEqual(result[0].id, "tag1") self.assertEqual(result[0].id, "tag1")
self.assertEqual(result[0].count, 11) self.assertEqual(result[0].count, 11)
self.assertEqual(result[1].id, "tag2") self.assertEqual(result[1].id, "tag2")
self.assertEqual(result[1].count, 5) self.assertEqual(result[1].count, 5)
@mock.patch.object(trovebox.Trovebox, 'get')
def test_empty_result(self, mock_get):
"""Check that an empty result is transformed into an empty list """
mock_get.return_value = self._return_value("")
result = self.client.tags.list(foo="bar")
mock_get.assert_called_with("/tags/list.json", foo="bar")
self.assertEqual(result, [])
@mock.patch.object(trovebox.Trovebox, 'get')
def test_zero_rows(self, mock_get):
"""Check that totalRows=0 is transformed into an empty list """
mock_get.return_value = self._return_value([{"totalRows": 0}])
result = self.client.tags.list(foo="bar")
mock_get.assert_called_with("/tags/list.json", foo="bar")
self.assertEqual(result, [])
class TestTagCreate(TestTags):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_tag_create(self, mock_post):
"""Check that a tag can be created"""
mock_post.return_value = self._return_value(True)
result = self.client.tag.create("test", foo="bar")
mock_post.assert_called_with("/tag/create.json", tag="test",
foo="bar")
self.assertEqual(result, True)
class TestTagDelete(TestTags): class TestTagDelete(TestTags):
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_tag_delete(self, mock_post): def test_tag_delete(self, mock_post):
"""Check that a tag can be deleted""" """Check that a tag can be deleted"""
mock_post.return_value = self._return_value(True) mock_post.return_value = self._return_value(True)
result = self.client.tag.delete(self.test_tags[0]) result = self.client.tag.delete(self.test_tags[0], foo="bar")
mock_post.assert_called_with("/tag/tag1/delete.json") mock_post.assert_called_with("/tag/tag1/delete.json", foo="bar")
self.assertEqual(result, True) self.assertEqual(result, True)
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_tag_delete_id(self, mock_post): def test_tag_delete_id(self, mock_post):
"""Check that a tag can be deleted using its ID""" """Check that a tag can be deleted using its ID"""
mock_post.return_value = self._return_value(True) mock_post.return_value = self._return_value(True)
result = self.client.tag.delete("tag1") result = self.client.tag.delete("tag1", foo="bar")
mock_post.assert_called_with("/tag/tag1/delete.json") mock_post.assert_called_with("/tag/tag1/delete.json", foo="bar")
self.assertEqual(result, True) self.assertEqual(result, True)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_tag_delete_failure(self, mock_post):
"""Check that an exception is raised if a tag cannot be deleted"""
mock_post.return_value = self._return_value(False)
with self.assertRaises(trovebox.TroveboxError):
self.client.tag.delete(self.test_tags[0])
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_tag_object_delete(self, mock_post): def test_tag_object_delete(self, mock_post):
"""Check that a tag can be deleted when using the tag object directly""" """Check that a tag can be deleted when using the tag object directly"""
mock_post.return_value = self._return_value(True) mock_post.return_value = self._return_value(True)
tag = self.test_tags[0] tag = self.test_tags[0]
result = tag.delete() result = tag.delete(foo="bar")
mock_post.assert_called_with("/tag/tag1/delete.json") mock_post.assert_called_with("/tag/tag1/delete.json", foo="bar")
self.assertEqual(result, True) self.assertEqual(result, True)
self.assertEqual(tag.get_fields(), {}) self.assertEqual(tag.get_fields(), {})
self.assertEqual(tag.id, None) self.assertEqual(tag.id, None)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_tag_object_delete_failure(self, mock_post):
"""
Check that an exception is raised if a tag cannot be deleted
when using the tag object directly
"""
mock_post.return_value = self._return_value(False)
with self.assertRaises(trovebox.TroveboxError):
self.test_tags[0].delete()
class TestTagUpdate(TestTags): class TestTagUpdate(TestTags):
@mock.patch.object(trovebox.Trovebox, 'post') @mock.patch.object(trovebox.Trovebox, 'post')
def test_tag_update(self, mock_post): def test_tag_update(self, mock_post):

11
tox.ini
View file

@ -1,5 +1,5 @@
[tox] [tox]
envlist = py26, py27, py33 envlist = py26, py27, py33, coverage
[testenv] [testenv]
commands = python -m unittest discover tests/unit commands = python -m unittest discover tests/unit
@ -18,3 +18,12 @@ deps =
ddt >= 0.3.0 ddt >= 0.3.0
unittest2 unittest2
discover discover
[testenv:coverage]
commands = coverage run --source trovebox setup.py test
deps =
mock >= 1.0.0
# Hold httpretty at 0.6.5 until https://github.com/gabrielfalcao/HTTPretty/issues/114 is resolved
httpretty == 0.6.5
ddt >= 0.3.0
coverage

View file

@ -0,0 +1,181 @@
diff --unified --recursive '--exclude=.pylint-disable.patch' original/api/api_activity.py patched/api/api_activity.py
--- original/api/api_activity.py
+++ patched/api/api_activity.py
@@ -32,7 +32,7 @@
"""
return self._client.post("/activities/purge.json", **kwds)["result"]
-class ApiActivity(ApiBase):
+class ApiActivity(ApiBase): # pylint: disable=too-few-public-methods
""" Definitions of /activity/ API endpoints """
def view(self, activity, **kwds):
"""
diff --unified --recursive '--exclude=.pylint-disable.patch' original/api/api_album.py patched/api/api_album.py
--- original/api/api_album.py
+++ patched/api/api_album.py
@@ -7,7 +7,7 @@
from trovebox.objects.album import Album
from .api_base import ApiBase
-class ApiAlbums(ApiBase):
+class ApiAlbums(ApiBase): # pylint: disable=too-few-public-methods
""" Definitions of /albums/ API endpoints """
def list(self, **kwds):
"""
diff --unified --recursive '--exclude=.pylint-disable.patch' original/api/api_base.py patched/api/api_base.py
--- original/api/api_base.py
+++ patched/api/api_base.py
@@ -2,7 +2,7 @@
api_base.py: Base class for all API classes
"""
-class ApiBase(object):
+class ApiBase(object): # pylint: disable=too-few-public-methods
""" Base class for all API objects """
def __init__(self, client):
self._client = client
diff --unified --recursive '--exclude=.pylint-disable.patch' original/api/api_tag.py patched/api/api_tag.py
--- original/api/api_tag.py
+++ patched/api/api_tag.py
@@ -2,14 +2,14 @@
api_tag.py : Trovebox Tag API Classes
"""
try:
- from urllib.parse import quote # Python3
+ from urllib.parse import quote # Python3 # pylint: disable=import-error,no-name-in-module
except ImportError:
from urllib import quote # Python2
from trovebox.objects.tag import Tag
from .api_base import ApiBase
-class ApiTags(ApiBase):
+class ApiTags(ApiBase): # pylint: disable=too-few-public-methods
""" Definitions of /tags/ API endpoints """
def list(self, **kwds):
"""
diff --unified --recursive '--exclude=.pylint-disable.patch' original/auth.py patched/auth.py
--- original/auth.py
+++ patched/auth.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import os
try:
- from configparser import ConfigParser # Python3
+ from configparser import ConfigParser # Python3 # pylint: disable=import-error
except ImportError:
from ConfigParser import SafeConfigParser as ConfigParser # Python2
try:
@@ -12,9 +12,9 @@
except ImportError: # pragma: no cover
import StringIO as io # Python2
-class Auth(object):
+class Auth(object): # pylint: disable=too-few-public-methods
"""OAuth secrets"""
- def __init__(self, config_file, host,
+ def __init__(self, config_file, host, # pylint: disable=too-many-arguments
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=maybe-no-member
except AttributeError:
parser.readfp(buf) # Python2
diff --unified --recursive '--exclude=.pylint-disable.patch' original/http.py patched/http.py
--- original/http.py
+++ patched/http.py
@@ -7,7 +7,7 @@
import requests_oauthlib
import logging
try:
- from urllib.parse import urlparse, urlunparse # Python3
+ from urllib.parse import urlparse, urlunparse # Python3 # pylint: disable=import-error,no-name-in-module
except ImportError:
from urlparse import urlparse, urlunparse # Python2
@@ -16,9 +16,9 @@
from .auth import Auth
if sys.version < '3':
- TEXT_TYPE = unicode
+ TEXT_TYPE = unicode # pylint: disable=invalid-name
else: # pragma: no cover
- TEXT_TYPE = str
+ TEXT_TYPE = str # pylint: disable=invalid-name
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=too-many-arguments
consumer_key='', consumer_secret='',
token='', token_secret='', api_version=None):
diff --unified --recursive '--exclude=.pylint-disable.patch' original/__init__.py patched/__init__.py
--- original/__init__.py
+++ patched/__init__.py
@@ -13,7 +13,7 @@
LATEST_API_VERSION = 2
-class Trovebox(Http):
+class Trovebox(Http): # pylint: disable=too-many-instance-attributes
"""
Client library for Trovebox
If no parameters are specified, config is loaded from the default
@@ -25,7 +25,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=too-many-arguments
consumer_key='', consumer_secret='',
token='', token_secret='',
api_version=None):
diff --unified --recursive '--exclude=.pylint-disable.patch' original/main.py patched/main.py
--- original/main.py
+++ patched/main.py
@@ -26,7 +26,7 @@
#################################################################
-def main(args=sys.argv[1:]):
+def main(args=sys.argv[1:]): # pylint: disable=too-many-branches
"""Run the commandline script"""
usage = "%prog --help"
parser = OptionParser(usage, add_help_option=False)
@@ -85,11 +85,11 @@
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=star-args
**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=star-args
files=files, **params)
for file_ in files:
files[file_].close()
diff --unified --recursive '--exclude=.pylint-disable.patch' original/objects/trovebox_object.py patched/objects/trovebox_object.py
--- original/objects/trovebox_object.py
+++ patched/objects/trovebox_object.py
@@ -5,7 +5,7 @@
""" Base object supporting the storage of custom fields as attributes """
_type = "None"
def __init__(self, client, json_dict):
- self.id = None
+ self.id = None # pylint: disable=invalid-name
self.name = None
self._client = client
self._json_dict = json_dict

View file

@ -1,238 +0,0 @@
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

@ -2,11 +2,14 @@
__init__.py : Trovebox package top level __init__.py : Trovebox package top level
""" """
from .http import Http from .http import Http
from .errors import * from .errors import TroveboxError, TroveboxDuplicateError, Trovebox404Error
from ._version import __version__ from ._version import __version__
from . import api_photo from trovebox.api import api_photo
from . import api_tag from trovebox.api import api_tag
from . import api_album from trovebox.api import api_album
from trovebox.api import api_action
from trovebox.api import api_activity
from trovebox.api import api_system
LATEST_API_VERSION = 2 LATEST_API_VERSION = 2
@ -36,3 +39,7 @@ class Trovebox(Http):
self.tag = api_tag.ApiTag(self) self.tag = api_tag.ApiTag(self)
self.albums = api_album.ApiAlbums(self) self.albums = api_album.ApiAlbums(self)
self.album = api_album.ApiAlbum(self) self.album = api_album.ApiAlbum(self)
self.action = api_action.ApiAction(self)
self.activities = api_activity.ApiActivities(self)
self.activity = api_activity.ApiActivity(self)
self.system = api_system.ApiSystem(self)

View file

@ -1,2 +1,2 @@
"""Current version string"""
__version__ = "0.5.1" __version__ = "0.6"

4
trovebox/api/__init__.py Normal file
View file

@ -0,0 +1,4 @@
"""
trovebox.api Package
Definitions for each of the Trovebox API endpoints
"""

View file

@ -0,0 +1,55 @@
"""
api_action.py : Trovebox Action API Classes
"""
from trovebox.objects.action import Action
from .api_base import ApiBase
class ApiAction(ApiBase):
""" Definitions of /action/ API endpoints """
def create(self, target, target_type=None, **kwds):
"""
Endpoint: /action/<target_id>/<target_type>/create.json
Creates a new action and returns it.
The target parameter can either be an id or a Trovebox object.
If a Trovebox object is used, the target type is inferred
automatically.
"""
# Extract the target type
if target_type is None:
target_type = target.get_type()
# Extract the target ID
try:
target_id = target.id
except AttributeError:
target_id = target
result = self._client.post("/action/%s/%s/create.json" %
(target_id, target_type),
**kwds)["result"]
return Action(self._client, result)
def delete(self, action, **kwds):
"""
Endpoint: /action/<id>/delete.json
Deletes an action.
Returns True if successful.
Raises a TroveboxError if not.
"""
return self._client.post("/action/%s/delete.json" %
self._extract_id(action),
**kwds)["result"]
def view(self, action, **kwds):
"""
Endpoint: /action/<id>/view.json
Requests all properties of an action.
Returns the requested action object.
"""
result = self._client.get("/action/%s/view.json" %
self._extract_id(action),
**kwds)["result"]
return Action(self._client, result)

View file

@ -0,0 +1,51 @@
"""
api_activity.py : Trovebox Activity API Classes
"""
import json
from trovebox.objects.activity import Activity
from .api_base import ApiBase
class ApiActivities(ApiBase):
""" Definitions of /activities/ API endpoints """
def list(self, options=None, **kwds):
"""
Endpoint: /activities[/<options>]/list.json
Returns a list of Activity objects.
The options parameter can be used to narrow down the activities.
Eg: options={"type": "photo-upload"}
"""
option_string = self._build_option_string(options)
activities = self._client.get("/activities%s/list.json" % option_string,
**kwds)["result"]
activities = self._result_to_list(activities)
return [Activity(self._client, activity) for activity in activities]
def purge(self, **kwds):
"""
Endpoint: /activities/purge.json
Purges all activities.
Returns True if successful.
Raises a TroveboxError if not.
Currently not working due to frontend issue #1368.
"""
return self._client.post("/activities/purge.json", **kwds)["result"]
class ApiActivity(ApiBase):
""" Definitions of /activity/ API endpoints """
def view(self, activity, **kwds):
"""
Endpoint: /activity/<id>/view.json
Requests all properties of an activity.
Returns the requested activity object.
"""
result = self._client.get("/activity/%s/view.json" %
self._extract_id(activity),
**kwds)["result"]
# TBD: Why is the result enclosed/encoded like this?
result = result["0"]
result["data"] = json.loads(result["data"])
return Activity(self._client, result)

152
trovebox/api/api_album.py Normal file
View file

@ -0,0 +1,152 @@
"""
api_album.py : Trovebox Album API Classes
"""
import collections
from trovebox.objects.trovebox_object import TroveboxObject
from trovebox.objects.album import Album
from .api_base import ApiBase
class ApiAlbums(ApiBase):
""" Definitions of /albums/ API endpoints """
def list(self, **kwds):
"""
Endpoint: /albums/list.json
Returns a list of Album objects.
"""
albums = self._client.get("/albums/list.json", **kwds)["result"]
albums = self._result_to_list(albums)
return [Album(self._client, album) for album in albums]
class ApiAlbum(ApiBase):
""" Definitions of /album/ API endpoints """
def cover_update(self, album, photo, **kwds):
"""
Endpoint: /album/<album_id>/cover/<photo_id>/update.json
Update the cover photo of an album.
Returns the updated album object.
"""
result = self._client.post("/album/%s/cover/%s/update.json" %
(self._extract_id(album),
self._extract_id(photo)),
**kwds)["result"]
# API currently doesn't return the updated album
# (frontend issue #1369)
if isinstance(result, bool): # pragma: no cover
result = self._client.get("/album/%s/view.json" %
self._extract_id(album))["result"]
return Album(self._client, result)
def create(self, name, **kwds):
"""
Endpoint: /album/create.json
Creates a new album and returns it.
"""
result = self._client.post("/album/create.json",
name=name, **kwds)["result"]
return Album(self._client, result)
def delete(self, album, **kwds):
"""
Endpoint: /album/<id>/delete.json
Deletes an album.
Returns True if successful.
Raises a TroveboxError if not.
"""
return self._client.post("/album/%s/delete.json" %
self._extract_id(album),
**kwds)["result"]
def add(self, album, objects, object_type=None, **kwds):
"""
Endpoint: /album/<id>/<type>/add.json
Add objects (eg. Photos) to an album.
The objects are a list of either IDs or Trovebox objects.
If Trovebox objects are used, the object type is inferred
automatically.
Returns the updated album object.
"""
return self._add_remove("add", album, objects, object_type,
**kwds)
def remove(self, album, objects, object_type=None, **kwds):
"""
Endpoint: /album/<id>/<type>/remove.json
Remove objects (eg. Photos) to an album.
The objects are a list of either IDs or Trovebox objects.
If Trovebox objects are used, the object type is inferred
automatically.
Returns the updated album object.
"""
return self._add_remove("remove", album, objects, object_type,
**kwds)
def _add_remove(self, action, album, objects, object_type=None,
**kwds):
"""Common code for the add and remove endpoints."""
# Ensure we have an iterable of objects
if not isinstance(objects, collections.Iterable):
objects = [objects]
# Extract the type of the objects
if object_type is None:
object_type = objects[0].get_type()
for i, obj in enumerate(objects):
if isinstance(obj, TroveboxObject):
# Ensure all objects are the same type
if obj.get_type() != object_type:
raise ValueError("Not all objects are of type '%s'"
% object_type)
# Extract the ids of the objects
objects[i] = obj.id
result = self._client.post("/album/%s/%s/%s.json" %
(self._extract_id(album),
object_type, action),
ids=objects, **kwds)["result"]
# API currently doesn't return the updated album
# (frontend issue #1369)
if isinstance(result, bool): # pragma: no cover
result = self._client.get("/album/%s/view.json" %
self._extract_id(album))["result"]
return Album(self._client, result)
def update(self, album, **kwds):
"""
Endpoint: /album/<id>/update.json
Updates an album with the specified parameters.
Returns the updated album object.
"""
result = self._client.post("/album/%s/update.json" %
self._extract_id(album),
**kwds)["result"]
# APIv1 doesn't return the updated album (frontend issue #937)
if isinstance(result, bool): # pragma: no cover
result = self._client.get("/album/%s/view.json" %
self._extract_id(album))["result"]
return Album(self._client, result)
def view(self, album, **kwds):
"""
Endpoint: /album/<id>/view.json
Requests all properties of an album.
Returns the requested album object.
"""
result = self._client.get("/album/%s/view.json" %
self._extract_id(album),
**kwds)["result"]
return Album(self._client, result)

38
trovebox/api/api_base.py Normal file
View file

@ -0,0 +1,38 @@
"""
api_base.py: Base class for all API classes
"""
class ApiBase(object):
""" Base class for all API objects """
def __init__(self, client):
self._client = client
@staticmethod
def _build_option_string(options):
"""
:param options: dictionary containing the options
:returns: option_string formatted for an API endpoint
"""
option_string = ""
if options is not None:
for key in options:
option_string += "/%s-%s" % (key, options[key])
return option_string
@staticmethod
def _extract_id(obj):
""" Return obj.id, or obj if the object doesn't have an ID """
try:
return obj.id
except AttributeError:
return obj
@staticmethod
def _result_to_list(result):
""" Handle the case where the result contains no items """
if not result:
return []
if "totalRows" in result[0] and result[0]["totalRows"] == 0:
return []
else:
return result

241
trovebox/api/api_photo.py Normal file
View file

@ -0,0 +1,241 @@
"""
api_photo.py : Trovebox Photo API Classes
"""
import base64
from trovebox.objects.photo import Photo
from .api_base import ApiBase
class ApiPhotos(ApiBase):
""" Definitions of /photos/ API endpoints """
def list(self, options=None, **kwds):
"""
Endpoint: /photos[/<options>]/list.json
Returns a list of Photo objects.
The options parameter can be used to narrow down the list.
Eg: options={"album": <album_id>}
"""
option_string = self._build_option_string(options)
photos = self._client.get("/photos%s/list.json" % option_string,
**kwds)["result"]
photos = self._result_to_list(photos)
return [Photo(self._client, photo) for photo in photos]
def share(self, options=None, **kwds):
"""
Endpoint: /photos[/<options>/share.json
Not currently implemented.
"""
option_string = self._build_option_string(options)
return self._client.post("/photos%s/share.json" % option_string,
**kwds)["result"]
def delete(self, photos, **kwds):
"""
Endpoint: /photos/delete.json
Deletes a list of photos.
Returns True if successful.
Raises a TroveboxError if not.
"""
ids = [self._extract_id(photo) for photo in photos]
return self._client.post("/photos/delete.json", ids=ids,
**kwds)["result"]
def update(self, photos, **kwds):
"""
Endpoint: /photos/<id>/update.json
Updates a list of photos with the specified parameters.
Returns True if successful.
Raises TroveboxError if not.
"""
ids = [self._extract_id(photo) for photo in photos]
return self._client.post("/photos/update.json", ids=ids,
**kwds)["result"]
class ApiPhoto(ApiBase):
""" Definitions of /photo/ API endpoints """
def delete(self, photo, **kwds):
"""
Endpoint: /photo/<id>/delete.json
Deletes a photo.
Returns True if successful.
Raises a TroveboxError if not.
"""
return self._client.post("/photo/%s/delete.json" %
self._extract_id(photo),
**kwds)["result"]
def delete_source(self, photo, **kwds):
"""
Endpoint: /photo/<id>/source/delete.json
Delete the source files of a photo.
Returns True if successful.
Raises a TroveboxError if not.
"""
return self._client.post("/photo/%s/source/delete.json" %
self._extract_id(photo),
**kwds)["result"]
def replace(self, photo, photo_file, **kwds):
"""
Endpoint: /photo/<id>/replace.json
Uploads the specified photo file to replace an existing photo.
"""
with open(photo_file, 'rb') as in_file:
result = self._client.post("/photo/%s/replace.json" %
self._extract_id(photo),
files={'photo': in_file},
**kwds)["result"]
return Photo(self._client, result)
def replace_encoded(self, photo, photo_file, **kwds):
"""
Endpoint: /photo/<id>/replace.json
Base64-encodes and uploads the specified photo filename to
replace an existing photo.
"""
with open(photo_file, "rb") as in_file:
encoded_photo = base64.b64encode(in_file.read())
result = self._client.post("/photo/%s/replace.json" %
self._extract_id(photo),
photo=encoded_photo,
**kwds)["result"]
return Photo(self._client, result)
def replace_from_url(self, photo, url, **kwds):
"""
Endpoint: /photo/<id>replace.json
Import a photo from the specified URL to replace an existing
photo.
"""
result = self._client.post("/photo/%s/replace.json" %
self._extract_id(photo),
photo=url,
**kwds)["result"]
return Photo(self._client, result)
def update(self, photo, **kwds):
"""
Endpoint: /photo/<id>/update.json
Updates a photo with the specified parameters.
Returns the updated photo object.
"""
result = self._client.post("/photo/%s/update.json" %
self._extract_id(photo),
**kwds)["result"]
return Photo(self._client, result)
def view(self, photo, options=None, **kwds):
"""
Endpoint: /photo/<id>[/<options>]/view.json
Requests all properties of a photo.
Can be used to obtain URLs for the photo at a particular size,
by using the "returnSizes" parameter.
Returns the requested photo object.
The options parameter can be used to pass in additional options.
Eg: options={"token": <token_data>}
"""
option_string = self._build_option_string(options)
result = self._client.get("/photo/%s%s/view.json" %
(self._extract_id(photo), option_string),
**kwds)["result"]
return Photo(self._client, result)
def upload(self, photo_file, **kwds):
"""
Endpoint: /photo/upload.json
Uploads the specified photo filename.
"""
with open(photo_file, 'rb') as in_file:
result = self._client.post("/photo/upload.json",
files={'photo': in_file},
**kwds)["result"]
return Photo(self._client, result)
def upload_encoded(self, photo_file, **kwds):
"""
Endpoint: /photo/upload.json
Base64-encodes and uploads the specified photo filename.
"""
with open(photo_file, "rb") as in_file:
encoded_photo = base64.b64encode(in_file.read())
result = self._client.post("/photo/upload.json", photo=encoded_photo,
**kwds)["result"]
return Photo(self._client, result)
def upload_from_url(self, url, **kwds):
"""
Endpoint: /photo/upload.json
Import a photo from the specified URL
"""
result = self._client.post("/photo/upload.json", photo=url,
**kwds)["result"]
return Photo(self._client, result)
def next_previous(self, photo, options=None, **kwds):
"""
Endpoint: /photo/<id>/nextprevious[/<options>].json
Returns a dict containing the next and previous photo lists
(there may be more than one next/previous photo returned).
The options parameter can be used to narrow down the photos
Eg: options={"album": <album_id>}
"""
option_string = self._build_option_string(options)
result = self._client.get("/photo/%s/nextprevious%s.json" %
(self._extract_id(photo), option_string),
**kwds)["result"]
value = {}
if "next" in result:
# Workaround for APIv1
if not isinstance(result["next"], list): # pragma: no cover
result["next"] = [result["next"]]
value["next"] = []
for photo in result["next"]:
value["next"].append(Photo(self._client, photo))
if "previous" in result:
# Workaround for APIv1
if not isinstance(result["previous"], list): # pragma: no cover
result["previous"] = [result["previous"]]
value["previous"] = []
for photo in result["previous"]:
value["previous"].append(Photo(self._client, photo))
return value
def transform(self, photo, **kwds):
"""
Endpoint: /photo/<id>/transform.json
Performs the specified transformations.
eg. transform(photo, rotate=90)
Returns the transformed photo.
"""
result = self._client.post("/photo/%s/transform.json" %
self._extract_id(photo),
**kwds)["result"]
# APIv1 doesn't return the transformed photo (frontend issue #955)
if isinstance(result, bool): # pragma: no cover
result = self._client.get("/photo/%s/view.json" %
self._extract_id(photo))["result"]
return Photo(self._client, result)

View file

@ -0,0 +1,27 @@
"""
api_system.py : Trovebox System API Classes
"""
from .api_base import ApiBase
class ApiSystem(ApiBase):
""" Definitions of /system/ API endpoints """
def version(self, **kwds):
"""
Endpoint: /system/version.json
Returns a dictionary containing the various server version strings
"""
return self._client.get("/system/version.json", **kwds)["result"]
def diagnostics(self, **kwds):
"""
Endpoint: /system/diagnostics.json
Runs a set of diagnostic tests on the server.
Returns a dictionary containing the results.
"""
# Don't process the result automatically, since this raises an exception
# on failure, which doesn't provide the cause of the failure
self._client.get("/system/diagnostics.json", process_response=False,
**kwds)
return self._client.last_response.json()["result"]

60
trovebox/api/api_tag.py Normal file
View file

@ -0,0 +1,60 @@
"""
api_tag.py : Trovebox Tag API Classes
"""
try:
from urllib.parse import quote # Python3
except ImportError:
from urllib import quote # Python2
from trovebox.objects.tag import Tag
from .api_base import ApiBase
class ApiTags(ApiBase):
""" Definitions of /tags/ API endpoints """
def list(self, **kwds):
"""
Endpoint: /tags/list.json
Returns a list of Tag objects.
"""
tags = self._client.get("/tags/list.json", **kwds)["result"]
tags = self._result_to_list(tags)
return [Tag(self._client, tag) for tag in tags]
class ApiTag(ApiBase):
""" Definitions of /tag/ API endpoints """
def create(self, tag, **kwds):
"""
Endpoint: /tag/create.json
Creates a new tag.
Returns True if successful.
Raises a TroveboxError if not.
"""
return self._client.post("/tag/create.json", tag=tag, **kwds)["result"]
def delete(self, tag, **kwds):
"""
Endpoint: /tag/<id>/delete.json
Deletes a tag.
Returns True if successful.
Raises a TroveboxError if not.
"""
return self._client.post("/tag/%s/delete.json" %
quote(self._extract_id(tag)),
**kwds)["result"]
def update(self, tag, **kwds):
"""
Endpoint: /tag/<id>/update.json
Updates a tag with the specified parameters.
Returns the updated tag object.
"""
result = self._client.post("/tag/%s/update.json" %
quote(self._extract_id(tag)),
**kwds)["result"]
return Tag(self._client, result)
# def view(self, tag, **kwds):

View file

@ -1,62 +0,0 @@
"""
api_album.py : Trovebox Album API Classes
"""
from .objects import Album
class ApiAlbums(object):
def __init__(self, client):
self._client = client
def list(self, **kwds):
""" Return a list of Album objects """
results = self._client.get("/albums/list.json", **kwds)["result"]
return [Album(self._client, album) for album in results]
class ApiAlbum(object):
def __init__(self, client):
self._client = client
def create(self, name, **kwds):
""" Create a new album and return it"""
result = self._client.post("/album/create.json",
name=name, **kwds)["result"]
return Album(self._client, result)
def delete(self, album, **kwds):
"""
Delete an album.
Returns True if successful.
Raises an TroveboxError if not.
"""
if not isinstance(album, Album):
album = Album(self._client, {"id": album})
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):
""" Update an album """
if not isinstance(album, Album):
album = Album(self._client, {"id": album})
album.update(**kwds)
return album
def view(self, album, **kwds):
"""
View an album's contents.
Returns the requested album object.
"""
if not isinstance(album, Album):
album = Album(self._client, {"id": album})
album.view(**kwds)
return album

View file

@ -1,142 +0,0 @@
"""
api_photo.py : Trovebox Photo API Classes
"""
import base64
from .errors import TroveboxError
from . import http
from .objects import Photo
def extract_ids(photos):
"""
Given a list of objects, extract the photo id for each Photo
object.
"""
ids = []
for photo in photos:
if isinstance(photo, Photo):
ids.append(photo.id)
else:
ids.append(photo)
return ids
class ApiPhotos(object):
def __init__(self, client):
self._client = client
def list(self, **kwds):
""" Returns a list of Photo objects """
photos = self._client.get("/photos/list.json", **kwds)["result"]
photos = http.result_to_list(photos)
return [Photo(self._client, photo) for photo in photos]
def update(self, photos, **kwds):
"""
Updates a list of photos.
Returns True if successful.
Raises TroveboxError if not.
"""
ids = extract_ids(photos)
if not self._client.post("/photos/update.json", ids=ids,
**kwds)["result"]:
raise TroveboxError("Update response returned False")
return True
def delete(self, photos, **kwds):
"""
Deletes a list of photos.
Returns True if successful.
Raises TroveboxError if not.
"""
ids = extract_ids(photos)
if not self._client.post("/photos/delete.json", ids=ids,
**kwds)["result"]:
raise TroveboxError("Delete response returned False")
return True
class ApiPhoto(object):
def __init__(self, client):
self._client = client
def delete(self, photo, **kwds):
"""
Delete a photo.
Returns True if successful.
Raises an TroveboxError if not.
"""
if not isinstance(photo, Photo):
photo = Photo(self._client, {"id": photo})
return photo.delete(**kwds)
def edit(self, photo, **kwds):
""" Returns an HTML form to edit a photo """
if not isinstance(photo, Photo):
photo = Photo(self._client, {"id": photo})
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):
"""
Update a photo with the specified parameters.
Returns the updated photo object
"""
if not isinstance(photo, Photo):
photo = Photo(self._client, {"id": photo})
photo.update(**kwds)
return photo
def view(self, photo, **kwds):
"""
Used to view the photo at a particular size.
Returns the requested photo object
"""
if not isinstance(photo, Photo):
photo = Photo(self._client, {"id": photo})
photo.view(**kwds)
return photo
def upload(self, photo_file, **kwds):
""" Uploads the specified file to the server """
with open(photo_file, 'rb') as in_file:
result = self._client.post("/photo/upload.json",
files={'photo': in_file},
**kwds)["result"]
return Photo(self._client, result)
def upload_encoded(self, photo_file, **kwds):
""" Base64-encodes and uploads the specified file """
with open(photo_file, "rb") as in_file:
encoded_photo = base64.b64encode(in_file.read())
result = self._client.post("/photo/upload.json", photo=encoded_photo,
**kwds)["result"]
return Photo(self._client, result)
def dynamic_url(self, photo, **kwds):
""" Not yet implemented """
raise NotImplementedError()
def next_previous(self, photo, **kwds):
"""
Returns a dict containing the next and previous photo lists
(there may be more than one next/previous photo returned).
"""
if not isinstance(photo, Photo):
photo = Photo(self._client, {"id": photo})
return photo.next_previous(**kwds)
def transform(self, photo, **kwds):
"""
Performs transformation specified in **kwds
Example: transform(photo, rotate=90)
"""
if not isinstance(photo, Photo):
photo = Photo(self._client, {"id": photo})
photo.transform(**kwds)
return photo

View file

@ -1,41 +0,0 @@
"""
api_tag.py : Trovebox Tag API Classes
"""
from .objects import Tag
class ApiTags(object):
def __init__(self, client):
self._client = client
def list(self, **kwds):
""" Returns a list of Tag objects """
results = self._client.get("/tags/list.json", **kwds)["result"]
return [Tag(self._client, tag) for tag in results]
class ApiTag(object):
def __init__(self, client):
self._client = client
def create(self, tag, **kwds):
"""
Create a new tag.
The API returns true if the tag was sucessfully created
"""
return self._client.post("/tag/create.json", tag=tag, **kwds)["result"]
def delete(self, tag, **kwds):
"""
Delete a tag.
Returns True if successful.
Raises an TroveboxError if not.
"""
if not isinstance(tag, Tag):
tag = Tag(self._client, {"id": tag})
return tag.delete(**kwds)
def update(self, tag, **kwds):
""" Update a tag """
if not isinstance(tag, Tag):
tag = Tag(self._client, {"id": tag})
tag.update(**kwds)
return tag

View file

@ -9,7 +9,7 @@ except ImportError:
from ConfigParser import SafeConfigParser as ConfigParser # Python2 from ConfigParser import SafeConfigParser as ConfigParser # Python2
try: try:
import io # Python3 import io # Python3
except ImportError: except ImportError: # pragma: no cover
import StringIO as io # Python2 import StringIO as io # Python2
class Auth(object): class Auth(object):

View file

@ -2,7 +2,7 @@
errors.py : Trovebox Error Classes errors.py : Trovebox Error Classes
""" """
class TroveboxError(Exception): class TroveboxError(Exception):
""" Indicates that an Trovebox operation failed """ """ Indicates that a Trovebox operation failed """
pass pass
class TroveboxDuplicateError(TroveboxError): class TroveboxDuplicateError(TroveboxError):

View file

@ -11,13 +11,13 @@ try:
except ImportError: except ImportError:
from urlparse import urlparse, urlunparse # Python2 from urlparse import urlparse, urlunparse # Python2
from .objects import TroveboxObject from trovebox.objects.trovebox_object import TroveboxObject
from .errors import * from .errors import TroveboxError, Trovebox404Error, TroveboxDuplicateError
from .auth import Auth from .auth import Auth
if sys.version < '3': if sys.version < '3':
TEXT_TYPE = unicode TEXT_TYPE = unicode
else: else: # pragma: no cover
TEXT_TYPE = str TEXT_TYPE = str
DUPLICATE_RESPONSE = {"code": 409, DUPLICATE_RESPONSE = {"code": 409,
@ -25,7 +25,7 @@ DUPLICATE_RESPONSE = {"code": 409,
class Http(object): class Http(object):
""" """
Base class to handle HTTP requests to an Trovebox server. Base class to handle HTTP requests to a Trovebox server.
If no parameters are specified, auth config is loaded from the If no parameters are specified, auth config is loaded from the
default 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.
@ -43,7 +43,7 @@ class Http(object):
self.config = dict(self._CONFIG_DEFAULTS) self.config = dict(self._CONFIG_DEFAULTS)
if api_version is not None: if api_version is not None: # pragma: no cover
print("Deprecation Warning: api_version should be set by " print("Deprecation Warning: api_version should be set by "
"calling the configure function") "calling the configure function")
self.config["api_version"] = api_version self.config["api_version"] = api_version
@ -104,7 +104,9 @@ class Http(object):
self._logger.info("============================") self._logger.info("============================")
self._logger.info("GET %s" % url) self._logger.info("GET %s" % url)
self._logger.info("---") self._logger.info("---")
self._logger.info(response.text) self._logger.info(response.text[:1000])
if len(response.text) > 1000: # pragma: no cover
self._logger.info("[Response truncated to 1000 characters]")
self.last_url = url self.last_url = url
self.last_params = params self.last_params = params
@ -113,7 +115,11 @@ class Http(object):
if process_response: if process_response:
return self._process_response(response) return self._process_response(response)
else: else:
return response.text if 200 <= response.status_code < 300:
return response.text
else:
raise TroveboxError("HTTP Error %d: %s" %
(response.status_code, response.reason))
def post(self, endpoint, process_response=True, files=None, **params): def post(self, endpoint, process_response=True, files=None, **params):
""" """
@ -154,7 +160,9 @@ class Http(object):
if files: if files:
self._logger.info("files: %s" % repr(files)) self._logger.info("files: %s" % repr(files))
self._logger.info("---") self._logger.info("---")
self._logger.info(response.text) self._logger.info(response.text[:1000])
if len(response.text) > 1000: # pragma: no cover
self._logger.info("[Response truncated to 1000 characters]")
self.last_url = url self.last_url = url
self.last_params = params self.last_params = params
@ -163,7 +171,11 @@ class Http(object):
if process_response: if process_response:
return self._process_response(response) return self._process_response(response)
else: else:
return response.text if 200 <= response.status_code < 300:
return response.text
else:
raise TroveboxError("HTTP Error %d: %s" %
(response.status_code, response.reason))
def _construct_url(self, endpoint): def _construct_url(self, endpoint):
"""Return the full URL to the specified endpoint""" """Return the full URL to the specified endpoint"""
@ -241,12 +253,3 @@ class Http(object):
raise TroveboxDuplicateError("Code %d: %s" % (code, message)) raise TroveboxDuplicateError("Code %d: %s" % (code, message))
else: else:
raise TroveboxError("Code %d: %s" % (code, message)) raise TroveboxError("Code %d: %s" % (code, message))
def result_to_list(result):
""" Handle the case where the result contains no items """
if not result:
return []
if result[0]["totalRows"] == 0:
return []
else:
return result

View file

@ -27,6 +27,7 @@ To get your credentials:
################################################################# #################################################################
def main(args=sys.argv[1:]): def main(args=sys.argv[1:]):
"""Run the commandline script"""
usage = "%prog --help" usage = "%prog --help"
parser = OptionParser(usage, add_help_option=False) parser = OptionParser(usage, add_help_option=False)
parser.add_option('-c', '--config', help="Configuration file to use", parser.add_option('-c', '--config', help="Configuration file to use",
@ -90,8 +91,8 @@ def main(args=sys.argv[1:]):
params, files = extract_files(params) params, files = extract_files(params)
result = client.post(options.endpoint, process_response=False, result = client.post(options.endpoint, process_response=False,
files=files, **params) files=files, **params)
for f in files: for file_ in files:
files[f].close() files[file_].close()
if options.verbose: if options.verbose:
print("==========\nMethod: %s\nHost: %s\nEndpoint: %s" % print("==========\nMethod: %s\nHost: %s\nEndpoint: %s" %
@ -129,5 +130,5 @@ def extract_files(params):
return updated_params, files return updated_params, files
if __name__ == "__main__": if __name__ == "__main__": # pragma: no cover
main() main()

View file

@ -1,236 +0,0 @@
"""
objects.py : Basic Trovebox API Objects
"""
try:
from urllib.parse import quote # Python3
except ImportError:
from urllib import quote # Python2
from .errors import TroveboxError
class TroveboxObject(object):
""" Base object supporting the storage of custom fields as attributes """
def __init__(self, trovebox, json_dict):
self.id = None
self.name = None
self._trovebox = trovebox
self._json_dict = json_dict
self._set_fields(json_dict)
def _set_fields(self, json_dict):
""" Set this object's attributes specified in json_dict """
for key, value in json_dict.items():
if not key.startswith("_"):
setattr(self, key, value)
def _replace_fields(self, json_dict):
"""
Delete this object's attributes, and replace with
those in json_dict.
"""
for key in self._json_dict.keys():
if not key.startswith("_"):
delattr(self, key)
self._json_dict = json_dict
self._set_fields(json_dict)
def _delete_fields(self):
"""
Delete this object's attributes, including name and id
"""
for key in self._json_dict.keys():
if not key.startswith("_"):
delattr(self, key)
self._json_dict = {}
self.id = None
self.name = None
def __repr__(self):
if self.name is not None:
return "<%s name='%s'>" % (self.__class__, self.name)
elif self.id is not None:
return "<%s id='%s'>" % (self.__class__, self.id)
else:
return "<%s>" % (self.__class__)
def get_fields(self):
""" Returns this object's attributes """
return self._json_dict
class Photo(TroveboxObject):
def delete(self, **kwds):
"""
Delete this photo.
Returns True if successful.
Raises an TroveboxError if not.
"""
result = self._trovebox.post("/photo/%s/delete.json" %
self.id, **kwds)["result"]
if not result:
raise TroveboxError("Delete response returned False")
self._delete_fields()
return result
def edit(self, **kwds):
""" Returns an HTML form to edit the photo """
result = self._trovebox.get("/photo/%s/edit.json" %
self.id, **kwds)["result"]
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):
""" Update this photo with the specified parameters """
new_dict = self._trovebox.post("/photo/%s/update.json" %
self.id, **kwds)["result"]
self._replace_fields(new_dict)
def view(self, **kwds):
"""
Used to view the photo at a particular size.
Updates the photo's fields with the response.
"""
new_dict = self._trovebox.get("/photo/%s/view.json" %
self.id, **kwds)["result"]
self._replace_fields(new_dict)
def dynamic_url(self, **kwds):
""" Not implemented yet """
raise NotImplementedError()
def next_previous(self, **kwds):
"""
Returns a dict containing the next and previous photo lists
(there may be more than one next/previous photo returned).
"""
result = self._trovebox.get("/photo/%s/nextprevious.json" %
self.id, **kwds)["result"]
value = {}
if "next" in result:
# Workaround for APIv1
if not isinstance(result["next"], list):
result["next"] = [result["next"]]
value["next"] = []
for photo in result["next"]:
value["next"].append(Photo(self._trovebox, photo))
if "previous" in result:
# Workaround for APIv1
if not isinstance(result["previous"], list):
result["previous"] = [result["previous"]]
value["previous"] = []
for photo in result["previous"]:
value["previous"].append(Photo(self._trovebox, photo))
return value
def transform(self, **kwds):
"""
Performs transformation specified in **kwds
Example: transform(rotate=90)
"""
new_dict = self._trovebox.post("/photo/%s/transform.json" %
self.id, **kwds)["result"]
# APIv1 doesn't return the transformed photo (frontend issue #955)
if isinstance(new_dict, bool):
new_dict = self._trovebox.get("/photo/%s/view.json" %
self.id)["result"]
self._replace_fields(new_dict)
class Tag(TroveboxObject):
def delete(self, **kwds):
"""
Delete this tag.
Returns True if successful.
Raises an TroveboxError if not.
"""
result = self._trovebox.post("/tag/%s/delete.json" %
quote(self.id), **kwds)["result"]
if not result:
raise TroveboxError("Delete response returned False")
self._delete_fields()
return result
def update(self, **kwds):
""" Update this tag with the specified parameters """
new_dict = self._trovebox.post("/tag/%s/update.json" % quote(self.id),
**kwds)["result"]
self._replace_fields(new_dict)
class Album(TroveboxObject):
def __init__(self, trovebox, json_dict):
self.photos = None
self.cover = None
TroveboxObject.__init__(self, trovebox, json_dict)
self._update_fields_with_objects()
def _update_fields_with_objects(self):
""" Convert dict fields into objects, where appropriate """
# Update the cover with a photo object
if isinstance(self.cover, dict):
self.cover = Photo(self._trovebox, self.cover)
# Update the photo list with photo objects
if isinstance(self.photos, list):
for i, photo in enumerate(self.photos):
if isinstance(photo, dict):
self.photos[i] = Photo(self._trovebox, photo)
def delete(self, **kwds):
"""
Delete this album.
Returns True if successful.
Raises an TroveboxError if not.
"""
result = self._trovebox.post("/album/%s/delete.json" %
self.id, **kwds)["result"]
if not result:
raise TroveboxError("Delete response returned False")
self._delete_fields()
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):
""" Update this album with the specified parameters """
new_dict = self._trovebox.post("/album/%s/update.json" %
self.id, **kwds)["result"]
# APIv1 doesn't return the updated album (frontend issue #937)
if isinstance(new_dict, bool):
new_dict = self._trovebox.get("/album/%s/view.json" %
self.id)["result"]
self._replace_fields(new_dict)
self._update_fields_with_objects()
def view(self, **kwds):
"""
Requests the full contents of the album.
Updates the album's fields with the response.
"""
result = self._trovebox.get("/album/%s/view.json" %
self.id, **kwds)["result"]
self._replace_fields(result)
self._update_fields_with_objects()

View file

@ -0,0 +1,4 @@
"""
trovebox.objects Package
Object classes returned by the API.
"""

View file

@ -0,0 +1,48 @@
"""
Representation of an Action object
"""
from .trovebox_object import TroveboxObject
from .photo import Photo
class Action(TroveboxObject):
""" Representation of an Action object """
_type = "action"
def __init__(self, client, json_dict):
self.target = None
self.target_type = None
TroveboxObject.__init__(self, client, json_dict)
self._update_fields_with_objects()
def _update_fields_with_objects(self):
""" Convert dict fields into objects, where appropriate """
# Update the photo target with photo objects
if self.target is not None:
if self.target_type == "photo":
self.target = Photo(self._client, self.target)
else:
raise NotImplementedError("Actions can only be assigned to "
"Photos")
def delete(self, **kwds):
"""
Endpoint: /action/<id>/delete.json
Deletes this action.
Returns True if successful.
Raises a TroveboxError if not.
"""
result = self._client.action.delete(self, **kwds)
self._delete_fields()
return result
def view(self, **kwds):
"""
Endpoint: /action/<id>/view.json
Requests the full contents of the action.
Updates the action object's fields with the response.
"""
result = self._client.action.view(self, **kwds)
self._replace_fields(result.get_fields())
self._update_fields_with_objects()

View file

@ -0,0 +1,37 @@
"""
Representation of an Activity object
"""
from .trovebox_object import TroveboxObject
from .photo import Photo
class Activity(TroveboxObject):
""" Representation of an Activity object """
_type = "activity"
def __init__(self, client, json_dict):
self.data = None
self.type = None
TroveboxObject.__init__(self, client, json_dict)
self._update_fields_with_objects()
def _update_fields_with_objects(self):
""" Convert dict fields into objects, where appropriate """
# Update the data with photo objects
if self.type is not None:
if self.type.startswith("photo"):
self.data = Photo(self._client, self.data)
else:
raise NotImplementedError("Unrecognised activity type: %s"
% self.type)
def view(self, **kwds):
"""
Endpoint: /activity/<id>/view.json
Requests the full contents of the activity.
Updates the activity's fields with the response.
"""
result = self._client.activity.view(self, **kwds)
self._replace_fields(result.get_fields())
self._update_fields_with_objects()

101
trovebox/objects/album.py Normal file
View file

@ -0,0 +1,101 @@
"""
Representation of an Album object
"""
from .trovebox_object import TroveboxObject
from .photo import Photo
class Album(TroveboxObject):
""" Representation of an Album object """
_type = "album"
def __init__(self, client, json_dict):
self.photos = None
self.cover = None
TroveboxObject.__init__(self, client, json_dict)
self._update_fields_with_objects()
def _update_fields_with_objects(self):
""" Convert dict fields into objects, where appropriate """
# Update the cover with a photo object
if isinstance(self.cover, dict):
self.cover = Photo(self._client, self.cover)
# Update the photo list with photo objects
try:
for i, photo in enumerate(self.photos):
if isinstance(photo, dict):
self.photos[i] = Photo(self._client, photo)
except (AttributeError, TypeError):
pass # No photos, or not a list
def cover_update(self, photo, **kwds):
"""
Endpoint: /album/<album_id>/cover/<photo_id>/update.json
Update the cover photo of this album.
"""
result = self._client.album.cover_update(self, photo, **kwds)
self._replace_fields(result.get_fields())
self._update_fields_with_objects()
def delete(self, **kwds):
"""
Endpoint: /album/<id>/delete.json
Deletes this album.
Returns True if successful.
Raises a TroveboxError if not.
"""
result = self._client.album.delete(self, **kwds)
self._delete_fields()
return result
def add(self, objects, object_type=None, **kwds):
"""
Endpoint: /album/<id>/<type>/add.json
Add objects (eg. Photos) to this album.
The objects are a list of either IDs or Trovebox objects.
If Trovebox objects are used, the object type is inferred
automatically.
Updates the album's fields with the response.
"""
result = self._client.album.add(self, objects, object_type, **kwds)
self._replace_fields(result.get_fields())
self._update_fields_with_objects()
def remove(self, objects, object_type=None, **kwds):
"""
Endpoint: /album/<id>/<type>/remove.json
Remove objects (eg. Photos) from this album.
The objects are a list of either IDs or Trovebox objects.
If Trovebox objects are used, the object type is inferred
automatically.
Updates the album's fields with the response.
"""
result = self._client.album.remove(self, objects, object_type,
**kwds)
self._replace_fields(result.get_fields())
self._update_fields_with_objects()
def update(self, **kwds):
"""
Endpoint: /album/<id>/update.json
Updates this album with the specified parameters.
"""
result = self._client.album.update(self, **kwds)
self._replace_fields(result.get_fields())
self._update_fields_with_objects()
def view(self, **kwds):
"""
Endpoint: /album/<id>/view.json
Requests all properties of an album.
Updates the album's fields with the response.
"""
result = self._client.album.view(self, **kwds)
self._replace_fields(result.get_fields())
self._update_fields_with_objects()

102
trovebox/objects/photo.py Normal file
View file

@ -0,0 +1,102 @@
"""
Representation of a Photo object
"""
from .trovebox_object import TroveboxObject
class Photo(TroveboxObject):
""" Representation of a Photo object """
_type = "photo"
def delete(self, **kwds):
"""
Endpoint: /photo/<id>/delete.json
Deletes this photo.
Returns True if successful.
Raises a TroveboxError if not.
"""
result = self._client.photo.delete(self, **kwds)
self._delete_fields()
return result
def delete_source(self, **kwds):
"""
Endpoint: /photo/<id>/source/delete.json
Deletes the source files of this photo.
Returns True if successful.
Raises a TroveboxError if not.
"""
return self._client.photo.delete_source(self, **kwds)
def replace(self, photo_file, **kwds):
"""
Endpoint: /photo/<id>/replace.json
Uploads the specified photo file to replace this photo.
"""
result = self._client.photo.replace(self, photo_file, **kwds)
self._replace_fields(result.get_fields())
def replace_encoded(self, photo_file, **kwds):
"""
Endpoint: /photo/<id>/replace.json
Base64-encodes and uploads the specified photo file to
replace this photo.
"""
result = self._client.photo.replace_encoded(self, photo_file,
**kwds)
self._replace_fields(result.get_fields())
def replace_from_url(self, url, **kwds):
"""
Endpoint: /photo/<id>replace.json
Import a photo from the specified URL to replace this photo.
"""
result = self._client.photo.replace_from_url(self, url, **kwds)
self._replace_fields(result.get_fields())
def update(self, **kwds):
"""
Endpoint: /photo/<id>/update.json
Updates this photo with the specified parameters.
"""
result = self._client.photo.update(self, **kwds)
self._replace_fields(result.get_fields())
def view(self, options=None, **kwds):
"""
Endpoint: /photo/<id>[/<options>]/view.json
Requests all properties of this photo.
Can be used to obtain URLs for the photo at a particular size,
by using the "returnSizes" parameter.
Updates the photo's fields with the response.
The options parameter can be used to pass in additional options.
Eg: options={"token": <token_data>}
"""
result = self._client.photo.view(self, options, **kwds)
self._replace_fields(result.get_fields())
def next_previous(self, options=None, **kwds):
"""
Endpoint: /photo/<id>/nextprevious[/<options>].json
Returns a dict containing the next and previous photo lists
(there may be more than one next/previous photo returned).
"""
return self._client.photo.next_previous(self, options, **kwds)
def transform(self, **kwds):
"""
Endpoint: /photo/<id>/transform.json
Performs the specified transformations.
eg. transform(photo, rotate=90)
Updates the photo's fields with the response.
"""
result = self._client.photo.transform(self, **kwds)
self._replace_fields(result.get_fields())

32
trovebox/objects/tag.py Normal file
View file

@ -0,0 +1,32 @@
"""
Representation of a Tag object
"""
from .trovebox_object import TroveboxObject
class Tag(TroveboxObject):
""" Representation of a Tag object """
_type = "tag"
def delete(self, **kwds):
"""
Endpoint: /tag/<id>/delete.json
Deletes this tag.
Returns True if successful.
Raises a TroveboxError if not.
"""
result = self._client.tag.delete(self, **kwds)
self._delete_fields()
return result
def update(self, **kwds):
"""
Endpoint: /tag/<id>/update.json
Updates this tag with the specified parameters.
Returns the updated tag object.
"""
result = self._client.tag.update(self, **kwds)
self._replace_fields(result.get_fields())
# def view(self, **kwds):

View file

@ -0,0 +1,56 @@
"""
Base object supporting the storage of custom fields as attributes
"""
class TroveboxObject(object):
""" Base object supporting the storage of custom fields as attributes """
_type = "None"
def __init__(self, client, json_dict):
self.id = None
self.name = None
self._client = client
self._json_dict = json_dict
self._set_fields(json_dict)
def _set_fields(self, json_dict):
""" Set this object's attributes specified in json_dict """
for key, value in json_dict.items():
if not key.startswith("_"):
setattr(self, key, value)
def _replace_fields(self, json_dict):
"""
Delete this object's attributes, and replace with
those in json_dict.
"""
for key in self._json_dict.keys():
if not key.startswith("_"):
delattr(self, key)
self._json_dict = json_dict
self._set_fields(json_dict)
def _delete_fields(self):
"""
Delete this object's attributes, including name and id
"""
for key in self._json_dict.keys():
if not key.startswith("_"):
delattr(self, key)
self._json_dict = {}
self.id = None
self.name = None
def __repr__(self):
if self.name is not None:
return "<%s name='%s'>" % (self.__class__.__name__, self.name)
elif self.id is not None:
return "<%s id='%s'>" % (self.__class__.__name__, self.id)
else:
return "<%s>" % (self.__class__.__name__)
def get_fields(self):
""" Returns this object's attributes """
return self._json_dict
def get_type(self):
""" Return this object's type (eg. "photo") """
return self._type