Merge pull request #47 from sneakypete81/release-0.4

Release 0.4
This commit is contained in:
sneakypete81 2013-07-20 10:26:08 -07:00
commit 3e5ba6ae28
44 changed files with 1872 additions and 497 deletions

View file

@ -4,15 +4,7 @@ install:
- pip install tox --use-mirrors
- .travis/install_pylint
env:
global:
- secure: "CQNSUBhVyedtCbioKkoueZsBFw0H+EnrPPNQIO+v3ieniPhFQbCoaCOA6rZG\n1MH1oIz5GSB1hv48jLCSSDppYOX1nKlLUFAepm9h7HSv2MaBzENKcp3ipBLP\nn8QEhVCkeWVnTkRB+IWrQHiW+8vHZ1iaERjlX9cMav7rBzzvK9g="
- secure: "e5xYBGGzn6x06hmofDJ+tuS8iAVPuFNGqizR8cA6+2W4rSQEbh7NcKKeAvB5\n8qlmBonupo0wttkewh2hpnxvaXV7uS4C0Qt/h087Bu4cPkJMENWq++CrDo6G\nwjkAu6x6YDkzuMuxa5BTWU9hAQVX1jq+cjYOmORhw/v5FFukN44="
- secure: "aU95NQmiY2ytyGRywEQvblN1YinIHpe/L9jnYlxazhfdHr+WXZd5aXC4Ze/U\nqlsHR+PGjycPHUCykJ/W5KU68tAX9r3PQgaQlfWd1cT89paY4givtoHiTz+f\nGu2I3BexskJ58NcUEDp6MEJqEuIXiQYUpoQ+6rNzvpe427xt6R0="
- secure: "ilNFM41mePkXMpvK/6T7s3vsQCN36XoiHnR7Fxrnpur9sXOfwB8A1Kw7CpbM\n5rxc2QNj7SPrT2K49QE8fUKHIl88a2MqCf+ujy9mG7WgKdxYazIxrhyHCNKO\nZ47r38kijW92GnSX4KTDeORfouZgR21BpDTfoCvspiWzWzG/fYE="
- secure: "YdUPDO7sTUTG2EwUlrxwOWKhlGXJiIK+RBWDspqvM8UQV4CQjzIsRX8urUIN\nSpSjJOfbIw25S+AsLpEBye8OJMncm/16Xp7PL5tlkNmRC12mPVG8f+wpOkrW\nt8v+2Cv/prYDn0tjoqnV1f5Nv5cEW6kAkG19UQ4QBgQzirtrs9Y="
script: .travis/run_travis
script: tox
after_script:
# Run Pylint

View file

@ -1,19 +0,0 @@
#!/bin/bash
# Create a config file containing the test host's secrets
CONFIG_DIR=~/.config/openphoto
CONFIG_FILE=$CONFIG_DIR/test
mkdir ~/.config
mkdir $CONFIG_DIR
echo "host = $OP_HOST" >> $CONFIG_FILE
echo "consumerKey = $OP_CONSUMER_KEY" >> $CONFIG_FILE
echo "consumerSecret = $OP_CONSUMER_SECRET" >> $CONFIG_FILE
echo "token = $OP_TOKEN" >> $CONFIG_FILE
echo "tokenSecret = $OP_TOKEN_SECRET" >> $CONFIG_FILE
# Run the tests
tox -e py27

View file

@ -1,131 +0,0 @@
Open Photo API / Python Library
=======================
#### OpenPhoto, a photo service for the masses
[![Build Status](https://api.travis-ci.org/photo/openphoto-python.png)](https://travis-ci.org/photo/openphoto-python)
----------------------------------------
<a name="install"></a>
### Installation
python setup.py install
----------------------------------------
<a name="credentials"></a>
### Credentials
For full access to your photos, you need to create the following config file in ``~/.config/openphoto/default``
# ~/.config/openphoto/default
host = your.host.com
consumerKey = your_consumer_key
consumerSecret = your_consumer_secret
token = your_access_token
tokenSecret = your_access_token_secret
The ``config_file`` switch lets you specify a different config file.
To get your credentials:
* Log into your Trovebox site
* Click the arrow on the top-right and select 'Settings'
* Click the 'Create a new app' button
* Click the 'View' link beside the newly created app
----------------------------------------
<a name="python"></a>
### How to use the library
You can use the library in one of two ways:
* Direct GET/POST calls to the server
* Access via Python classes/methods
<a name="get_post"></a>
#### Direct GET/POST:
from openphoto import OpenPhoto
client = OpenPhoto()
resp = client.get("/photos/list.json")
resp = client.post("/photo/62/update.json", tags=["tag1", "tag2"])
<a name="python_classes"></a>
#### Python classes/methods
from openphoto import OpenPhoto
client = OpenPhoto()
photos = client.photos.list()
photos[0].update(tags=["tag1", "tag2"])
print(photos[0].tags)
The OpenPhoto Python class hierarchy mirrors the [OpenPhoto API](http://theopenphotoproject.org/documentation) endpoint layout. For example, the calls in the example above use the following API endpoints:
* ``client.photos.list() -> /photos/list.json``
* ``photos[0].update() -> /photo/<id>/update.json``
<a name="api_versioning"></a>
### API Versioning
It may be useful to lock your application to a particular version of the OpenPhoto API.
This ensures that future API updates won't cause unexpected breakages.
To do this, add the optional ```api_version``` parameter when creating the client object:
from openphoto import OpenPhoto
client = OpenPhoto(api_version=2)
----------------------------------------
<a name="cli"></a>
### Using from the command line
You can run commands to the OpenPhoto API from your shell!
These are the options you can pass to the shell program:
--help # Display help text
-c config_file # Either the name of a config file in ~/.config/openphoto/ or a full path to a config file
-h hostname # Overrides config_file for unauthenticated API calls
-e endpoint # [default=/photos/list.json]
-X method # [default=GET]
-F params # e.g. -F 'title=my title' -F 'tags=mytag1,mytag2'
-p # Pretty print the json
-v # Verbose output
<a name="cli-examples"></a>
#### Command line examples
# Upload a public photo to the host specified in ~/.config/openphoto/default
openphoto -p -X POST -e /photo/upload.json -F 'photo=@/path/to/photo/jpg' -F 'permission=1'
{
"code":201,
"message":"Photo 1eo uploaded successfully",
"result":{
"actor":"user@example.com",
"albums":[],
...
...
}
}
# Get a thumbnail URL from current.openphoto.me (unauthenticated access)
openphoto -h current.openphoto.me -p -e /photo/62/view.json -F 'returnSizes=20x20'
{
"code":200,
"message":"Photo 62",
"result":{
"actor":"",
"albums":[
"1"
],
...
...
"path20x20":"http://current.openphoto.me/photo/62/create/36c0a/20x20.jpg",
"pathBase":"http://awesomeness.openphoto.me/base/201203/7ae997-Boracay-Philippines-007.jpg",
"permission":"1",
"photo20x20":[
"http://current.openphoto.me/photo/62/create/36c0a/20x20.jpg",
13,
20
],
...
...
}
}

142
README.rst Normal file
View file

@ -0,0 +1,142 @@
=======================
Trovebox Python Library
=======================
(Previously known as openphoto-python)
.. image:: https://api.travis-ci.org/photo/openphoto-python.png
:alt: Build Status
:target: https://travis-ci.org/photo/openphoto-python
.. image:: https://pypip.in/v/trovebox/badge.png
:alt: Python Package Index (PyPI)
:target: https://pypi.python.org/pypi/trovebox
This library works with any Trovebox server, either
`self-hosted <https://github.com/photo>`__, or using the hosted service at
`trovebox.com <http://trovebox.com>`__.
It provides full access to your photos and metadata, via a simple
Pythonic API.
Installation
============
::
pip install trovebox
Documentation
=============
See the `Trovebox API Documentation <https://trovebox.com/documentation>`__
for full API documentation, including Python examples.
All development takes place at the `openphoto-python GitHub site <https://github.com/photo/openphoto-python>`__.
Credentials
===========
For full access to your photos, you need to create the following config
file in ``~/.config/trovebox/default``::
# ~/.config/trovebox/default
host = your.host.com
consumerKey = your_consumer_key
consumerSecret = your_consumer_secret
token = your_access_token
tokenSecret = your_access_token_secret
The ``config_file`` switch lets you specify a different config file.
To get your credentials:
* Log into your Trovebox site
* Click the arrow on the top-right and select 'Settings'
* Click the 'Create a new app' button
* Click the 'View' link beside the newly created app
Using the library
=================
::
from trovebox import Trovebox
client = Trovebox()
photos = client.photos.list()
photos[0].update(tags=["tag1", "tag2"])
print(photos[0].tags)
The Trovebox Python class hierarchy mirrors the
`Trovebox API <https://trovebox.com/documentation>`__ endpoint layout.
For example, the calls in the example above use the following API endpoints:
* ``client.photos.list() -> /photos/list.json``
* ``photos[0].update() -> /photo/<id>/update.json``
You can also access the API at a lower level using GET/POST methods::
resp = client.get("/photos/list.json")
resp = client.post("/photo/62/update.json", tags=["tag1", "tag2"])
API Versioning
==============
It may be useful to lock your application to a particular version of the Trovebox API.
This ensures that future API updates won't cause unexpected breakages.
To do this, add the optional ``api_version`` parameter when creating the client object::
from trovebox import Trovebox
client = Trovebox(api_version=2)
Commandline Tool
================
You can run commands to the Trovebox API from your shell!
These are the options you can pass to the shell program::
--help # Display help text
-c config_file # Either the name of a config file in ~/.config/trovebox/ or a full path to a config file
-h hostname # Overrides config_file for unauthenticated API calls
-e endpoint # [default=/photos/list.json]
-X method # [default=GET]
-F params # e.g. -F 'title=my title' -F 'tags=mytag1,mytag2'
-p # Pretty print the json
-v # Verbose output
--version # Display the current version information
Commandline Examples
--------------------
Upload a public photo to the host specified in ```~/.config/trovebox/default```::
trovebox -p -X POST -e /photo/upload.json -F 'photo=@/path/to/photo/jpg' -F 'permission=1'
{
"code":201,
"message":"Photo 1eo uploaded successfully",
"result":{
"actor":"user@example.com",
"albums":[],
...
...
}
}
Get a thumbnail URL from current.trovebox.com (unauthenticated access)::
trovebox -h current.trovebox.com -p -e /photo/62/view.json -F 'returnSizes=20x20'
{
"code":200,
"message":"Photo 62",
"result":{
"actor":"",
"albums":[
"1"
],
...
...
"path20x20":"http://current.trovebox.com/photo/62/create/36c0a/20x20.jpg",
"pathBase":"http://awesomeness.trovebox.com/base/201203/7ae997-Boracay-Philippines-007.jpg",
"permission":"1",
"photo20x20":[
"http://current.trovebox.com/photo/62/create/36c0a/20x20.jpg",
13,
20
],
...
...
}
}

4
bin/trovebox Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env python
import trovebox.main
trovebox.main.main()

View file

@ -1,34 +0,0 @@
from openphoto.openphoto_http import OpenPhotoHttp
from openphoto.errors import *
import openphoto.api_photo
import openphoto.api_tag
import openphoto.api_album
LATEST_API_VERSION = 2
class OpenPhoto(OpenPhotoHttp):
"""
Client library for OpenPhoto
If no parameters are specified, config is loaded from the default
location (~/.config/openphoto/default).
The config_file parameter is used to specify an alternate config file.
If the host parameter is specified, no config file is loaded and
OAuth tokens (consumer*, token*) can optionally be specified.
All requests will include the api_version path, if specified.
This should be used to ensure that your application will continue to work
even if the OpenPhoto API is updated to a new revision.
"""
def __init__(self, config_file=None, host=None,
consumer_key='', consumer_secret='',
token='', token_secret='',
api_version=None):
OpenPhotoHttp.__init__(self, config_file, host,
consumer_key, consumer_secret,
token, token_secret, api_version)
self.photos = openphoto.api_photo.ApiPhotos(self)
self.photo = openphoto.api_photo.ApiPhoto(self)
self.tags = openphoto.api_tag.ApiTags(self)
self.tag = openphoto.api_tag.ApiTag(self)
self.albums = openphoto.api_album.ApiAlbums(self)
self.album = openphoto.api_album.ApiAlbum(self)

44
run_functional_tests Executable file
View file

@ -0,0 +1,44 @@
#!/bin/bash
#
# Simple script to run all functional tests with multiple test servers
#
# Test server running latest self-hosted site
# Install from latest photo/frontend master commit
tput setaf 3
echo
echo "Testing latest self-hosted site..."
tput sgr0
export TROVEBOX_TEST_CONFIG=test
unset TROVEBOX_TEST_SERVER_API
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
# Install from photo/frontend commit e9d81de57b
tput setaf 3
echo
echo "Testing v3.0.8 self-hosted site..."
tput sgr0
export TROVEBOX_TEST_CONFIG=test-3.0.8
unset TROVEBOX_TEST_SERVER_API
python -m unittest discover --catch tests/functional
# Test account on hosted trovebox.com site
tput setaf 3
echo
echo "Testing latest hosted site..."
tput sgr0
export TROVEBOX_TEST_CONFIG=test-hosted
unset TROVEBOX_TEST_SERVER_API
python -m unittest discover --catch tests/functional

View file

@ -1,34 +0,0 @@
#!/bin/bash
#
# Simple script to run all tests with multiple test servers
# across all supported Python versions
#
# Default test server running latest self-hosted site
tput setaf 3
echo
echo "Testing latest self-hosted site..."
tput sgr0
export OPENPHOTO_TEST_CONFIG=test
unset OPENPHOTO_TEST_SERVER_API
tox $@
# Test server running APIv1 OpenPhoto instance
tput setaf 3
echo
echo "Testing APIv1 self-hosted site..."
tput sgr0
export OPENPHOTO_TEST_CONFIG=test-apiv1
export OPENPHOTO_TEST_SERVER_API=1
tox $@
# Test account on hosted trovebox.com site
tput setaf 3
echo
echo "Testing latest hosted site..."
tput sgr0
export OPENPHOTO_TEST_CONFIG=test-hosted
unset OPENPHOTO_TEST_SERVER_API
tox $@

View file

@ -1,4 +0,0 @@
#!/usr/bin/env python
import openphoto.main
openphoto.main.main()

View file

@ -1,25 +1,50 @@
#!/usr/bin/env python
import sys
requires = ['requests', 'requests-oauthlib']
requires = ['requests', 'requests_oauthlib']
console_script = """[console_scripts]
trovebox = trovebox.main:main
"""
# from trovebox._version import __version__
exec(open("trovebox/_version.py").read())
# Check the Python version
(major, minor) = sys.version_info[:2]
if (major, minor) < (2, 6):
raise SystemExit("Sorry, Python 2.6 or newer required")
try:
from setuptools import setup
kw = {'entry_points':
"""[console_scripts]\nopenphoto = openphoto.main:main\n""",
'zip_safe': False,
kw = {'entry_points': console_script,
'zip_safe': True,
'install_requires': requires
}
except ImportError:
from distutils.core import setup
kw = {'scripts': ['scripts/openphoto'],
kw = {'scripts': ['bin/trovebox'],
'requires': requires}
setup(name='openphoto',
version='0.3',
description='Client library for the openphoto project',
author='James Walker',
author_email='walkah@walkah.net',
url='https://github.com/openphoto/openphoto-python',
packages=['openphoto'],
setup(name='trovebox',
version=__version__,
description='The official Python client library for the Trovebox photo service',
long_description=open("README.rst").read(),
author='Pete Burgers, James Walker',
url='https://github.com/photo/openphoto-python',
packages=['trovebox'],
data_files=['README.rst'],
keywords=['openphoto', 'pyopenphoto', 'openphoto-python',
'trovebox', 'pytrovebox', 'trovebox-python'],
classifiers=['Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: OSI Approved :: Apache Software License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Multimedia :: Graphics',
'Topic :: Software Development :: Libraries :: Python Modules',
],
license='Apache 2.0',
test_suite='tests.unit',
**kw
)

View file

@ -1,101 +1,34 @@
Tests for the Open Photo API / Python Library
Trovebox Python Testing
=======================
#### OpenPhoto, a photo service for the masses
###Unit Tests
The unit tests mock out all HTTP requests, and verify that the various
components of the library are operating correctly.
They run very quickly and don't require any external test hosts.
<a name="requirements"></a>
#### Requirements
* mock >= 1.0.0
* httpretty >= 0.6.1
* tox (optional)
#### Running the Unit Tests
python -m unittest discover tests/unit
To run the unit tests against all supported Python versions, use ```tox```:
tox
----------------------------------------
<a name="requirements"></a>
### Requirements
A computer, Python and an empty OpenPhoto test host.
---------------------------------------
<a name="setup"></a>
### Setting up
###Functional Tests
Create a ``~/.config/openphoto/test`` config file containing the following:
The functional tests check that the Trovebox python library interoperates
correctly with a real Trovebox server.
# ~/.config/openphoto/test
host = your.host.com
consumerKey = your_consumer_key
consumerSecret = your_consumer_secret
token = your_access_token
tokenSecret = your_access_token_secret
They are slow to run and rely on a stable HTTP connection to a test server.
Make sure this is an empty test server, **not a production OpenPhoto server!!!**
You can specify an alternate test config file with the following environment variable:
export OPENPHOTO_TEST_CONFIG=test2
---------------------------------------
<a name="running"></a>
### Running the tests
The following instructions are for Python 2.7. You can adapt them for earlier
Python versions using the ``unittest2`` package.
cd /path/to/openphoto-python
python -m unittest discover -c
The "-c" lets you stop the tests gracefully with \[CTRL\]-c.
The easiest way to run a subset of the tests is with the ``nose`` package:
cd /path/to/openphoto-python
nosetests -v -s --nologcapture tests/test_albums.py:TestAlbums.test_view
All HTTP requests and responses are recorded in the file ``tests.log``.
You can enable more verbose output to stdout with the following environment variable:
export OPENPHOTO_TEST_DEBUG=1
---------------------------------------
<a name="test_details"></a>
### Test Details
These tests are intended to verify the openphoto-python library.
They don't provide comprehensive testing of the OpenPhoto API,
there are PHP unit tests for that.
Each test class is run as follows:
**SetUpClass:**
Check that the server is empty
**SetUp:**
Ensure there are:
* Three test photos
* A single test tag applied to each
* A single album containing all three photos
**TearDownClass:**
Remove all photos, tags and albums
### Testing old servers
By default, all currently supported API versions will be tested.
It's useful to test servers that only support older API versions.
To restrict the testing to a specific maximum API version, use the
``OPENPHOTO_TEST_SERVER_API`` environment variable.
For example, to restrict testing to APIv1 and APIv2:
export OPENPHOTO_TEST_SERVER_API=2
<a name="full_regression"></a>
### Full Regression Test
The ``run_tests`` script uses the ``tox`` package to run a full regression across:
* Multiple Python versions
* All supported API versions
To use it, you must set up multiple OpenPhoto instances and create the following
config files containing your credentials:
test : Latest self-hosted site
test-apiv1 : APIv1 self-hosted site
test-hosted : Credentials for test account on trovebox.com
For full details, see the [functional test README file](functional/README.markdown).

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 910 B

After

Width:  |  Height:  |  Size: 910 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 635 B

After

Width:  |  Height:  |  Size: 635 B

Before After
Before After

View file

@ -0,0 +1,105 @@
Functional Testing
=======================
These functional tests check that the Trovebox python library interoperates
correctly with a real Trovebox server.
They are slow to run, and require a stable HTTP connection to a test server.
----------------------------------------
<a name="requirements"></a>
### Requirements
A computer, Python and an empty Trovebox test host.
---------------------------------------
<a name="setup"></a>
### Setting up
Create a ``~/.config/trovebox/test`` config file containing the following:
# ~/.config/trovebox/test
host = your.host.com
consumerKey = your_consumer_key
consumerSecret = your_consumer_secret
token = your_access_token
tokenSecret = your_access_token_secret
Make sure this is an empty test server, **not a production Trovebox server!!!**
You can specify an alternate test config file with the following environment variable:
export TROVEBOX_TEST_CONFIG=test2
---------------------------------------
<a name="running"></a>
### Running the tests
The following instructions are for Python 2.7. You can adapt them for earlier
Python versions using the ``unittest2`` package.
cd /path/to/trovebox-python
python -m unittest discover -c tests/functional
The "-c" lets you stop the tests gracefully with \[CTRL\]-c.
The easiest way to run a subset of the tests is with the ``nose`` package:
cd /path/to/trovebox-python
nosetests -v -s --nologcapture tests/functional/test_albums.py:TestAlbums.test_view
All HTTP requests and responses are recorded in the file ``tests.log``.
You can enable more verbose output to stdout with the following environment variable:
export TROVEBOX_TEST_DEBUG=1
---------------------------------------
<a name="test_details"></a>
### Test Details
These tests are intended to verify the Trovebox python library.
They don't provide comprehensive testing of the Trovebox API,
there are PHP unit tests for that.
Each test class is run as follows:
**SetUpClass:**
Check that the server is empty
**SetUp:**
Ensure there are:
* Three test photos
* A single test tag applied to each
* A single album containing all three photos
**TearDownClass:**
Remove all photos, tags and albums
### Testing old servers
By default, all currently supported API versions will be tested.
It's useful to test servers that only support older API versions.
To restrict the testing to a specific maximum API version, use the
``TROVEBOX_TEST_SERVER_API`` environment variable.
For example, to restrict testing to APIv1 and APIv2:
export TROVEBOX_TEST_SERVER_API=2
<a name="full_regression"></a>
### Full Regression Test
The ``run_functional_tests`` script runs all functional tests against
all supported API versions.
To use it, you must set up multiple Trovebox instances and create the following
config files containing your credentials:
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-hosted : Credentials for test account on trovebox.com

View file

@ -1,4 +1,4 @@
from tests import test_albums, test_photos, test_tags
from tests.functional import test_albums, test_photos, test_tags
class TestAlbumsV1(test_albums.TestAlbums):
api_version = 1

View file

@ -2,7 +2,7 @@ try:
import unittest2 as unittest
except ImportError:
import unittest
from tests import test_base, test_albums, test_photos, test_tags
from tests.functional import test_base, test_albums, test_photos, test_tags
@unittest.skipIf(test_base.get_test_server_api() < 2,
"Don't test future API versions")

View file

@ -1,6 +1,6 @@
import tests.test_base
from tests.functional import test_base
class TestAlbums(tests.test_base.TestBase):
class TestAlbums(test_base.TestBase):
testcase_name = "album API"
def test_create_delete(self):
@ -29,7 +29,7 @@ class TestAlbums(tests.test_base.TestBase):
def test_update(self):
""" Test that an album can be updated """
# Update the album using the OpenPhoto class,
# Update the album using the Trovebox class,
# passing in the album object
new_name = "New Name"
self.client.album.update(self.albums[0], name=new_name)
@ -38,7 +38,7 @@ class TestAlbums(tests.test_base.TestBase):
self.albums = self.client.albums.list()
self.assertEqual(self.albums[0].name, new_name)
# Update the album using the OpenPhoto class, passing in the album id
# Update the album using the Trovebox class, passing in the album id
new_name = "Another New Name"
self.client.album.update(self.albums[0].id, name=new_name)

View file

@ -7,11 +7,11 @@ try:
except ImportError:
import unittest
import openphoto
import trovebox
def get_test_server_api():
return int(os.getenv("OPENPHOTO_TEST_SERVER_API",
openphoto.LATEST_API_VERSION))
return int(os.getenv("TROVEBOX_TEST_SERVER_API",
trovebox.LATEST_API_VERSION))
class TestBase(unittest.TestCase):
TEST_TITLE = "Test Image - delete me!"
@ -21,8 +21,8 @@ class TestBase(unittest.TestCase):
testcase_name = "(unknown testcase)"
api_version = None
config_file = os.getenv("OPENPHOTO_TEST_CONFIG", "test")
debug = (os.getenv("OPENPHOTO_TEST_DEBUG", "0") == "1")
config_file = os.getenv("TROVEBOX_TEST_CONFIG", "test")
debug = (os.getenv("TROVEBOX_TEST_DEBUG", "0") == "1")
def __init__(self, *args, **kwds):
super(TestBase, self).__init__(*args, **kwds)
@ -42,7 +42,7 @@ class TestBase(unittest.TestCase):
else:
print("\nTesting %s v%d" % (cls.testcase_name, cls.api_version))
cls.client = openphoto.OpenPhoto(config_file=cls.config_file,
cls.client = trovebox.Trovebox(config_file=cls.config_file,
api_version=cls.api_version)
if cls.client.photos.list() != []:
@ -128,13 +128,13 @@ class TestBase(unittest.TestCase):
""" Upload three test photos """
album = cls.client.album.create(cls.TEST_ALBUM)
photos = [
cls.client.photo.upload("tests/test_photo1.jpg",
cls.client.photo.upload("tests/data/test_photo1.jpg",
title=cls.TEST_TITLE,
albums=album.id),
cls.client.photo.upload("tests/test_photo2.jpg",
cls.client.photo.upload("tests/data/test_photo2.jpg",
title=cls.TEST_TITLE,
albums=album.id),
cls.client.photo.upload("tests/test_photo3.jpg",
cls.client.photo.upload("tests/data/test_photo3.jpg",
title=cls.TEST_TITLE,
albums=album.id),
]

View file

@ -1,9 +1,9 @@
import logging
import openphoto
import tests.test_base
import trovebox
from tests.functional import test_base
class TestFramework(tests.test_base.TestBase):
class TestFramework(test_base.TestBase):
testcase_name = "framework"
def setUp(self):
@ -16,7 +16,7 @@ class TestFramework(tests.test_base.TestBase):
"""
API v0 has a special hello world message
"""
client = openphoto.OpenPhoto(config_file=self.config_file,
client = trovebox.Trovebox(config_file=self.config_file,
api_version=0)
result = client.get("hello.json")
self.assertEqual(result['message'],
@ -27,8 +27,8 @@ class TestFramework(tests.test_base.TestBase):
"""
For all API versions >0, we get a generic hello world message
"""
for api_version in range(1, tests.test_base.get_test_server_api() + 1):
client = openphoto.OpenPhoto(config_file=self.config_file,
for api_version in range(1, test_base.get_test_server_api() + 1):
client = trovebox.Trovebox(config_file=self.config_file,
api_version=api_version)
result = client.get("hello.json")
self.assertEqual(result['message'], "Hello, world!")
@ -40,7 +40,7 @@ class TestFramework(tests.test_base.TestBase):
If the API version is unspecified,
we get a generic hello world message.
"""
client = openphoto.OpenPhoto(config_file=self.config_file,
client = trovebox.Trovebox(config_file=self.config_file,
api_version=None)
result = client.get("hello.json")
self.assertEqual(result['message'], "Hello, world!")
@ -51,7 +51,8 @@ class TestFramework(tests.test_base.TestBase):
If the API version is unsupported, we should get an error
(ValueError, since the returned 404 HTML page is not valid JSON)
"""
client = openphoto.OpenPhoto(config_file=self.config_file,
api_version=openphoto.LATEST_API_VERSION + 1)
with self.assertRaises(openphoto.OpenPhoto404Error):
version = trovebox.LATEST_API_VERSION + 1
client = trovebox.Trovebox(config_file=self.config_file,
api_version=version)
with self.assertRaises(trovebox.Trovebox404Error):
client.get("hello.json")

View file

@ -1,16 +1,16 @@
from __future__ import unicode_literals
import openphoto
import tests.test_base
import trovebox
from tests.functional import test_base
class TestPhotos(tests.test_base.TestBase):
class TestPhotos(test_base.TestBase):
testcase_name = "photo API"
def test_delete_upload(self):
""" Test photo deletion and upload """
# Delete one photo using the OpenPhoto class, passing in the id
# Delete one photo using the Trovebox class, passing in the id
self.assertTrue(self.client.photo.delete(self.photos[0].id))
# Delete one photo using the OpenPhoto class, passing in the object
# Delete one photo using the Trovebox class, passing in the object
self.assertTrue(self.client.photo.delete(self.photos[1]))
# And another using the Photo object directly
self.assertTrue(self.photos[2].delete())
@ -19,11 +19,11 @@ class TestPhotos(tests.test_base.TestBase):
self.assertEqual(self.client.photos.list(), [])
# Re-upload the photos, one of them using Bas64 encoding
ret_val = self.client.photo.upload("tests/test_photo1.jpg",
ret_val = self.client.photo.upload("tests/data/test_photo1.jpg",
title=self.TEST_TITLE)
self.client.photo.upload("tests/test_photo2.jpg",
self.client.photo.upload("tests/data/test_photo2.jpg",
title=self.TEST_TITLE)
self.client.photo.upload_encoded("tests/test_photo3.jpg",
self.client.photo.upload_encoded("tests/data/test_photo3.jpg",
title=self.TEST_TITLE)
# Check there are now three photos with the correct titles
@ -49,7 +49,7 @@ class TestPhotos(tests.test_base.TestBase):
def test_edit(self):
""" Check that the edit request returns an HTML form """
# Test using the OpenPhoto class
# Test using the Trovebox class
html = self.client.photo.edit(self.photos[0])
self.assertIn("<form", html.lower())
@ -60,8 +60,8 @@ class TestPhotos(tests.test_base.TestBase):
def test_upload_duplicate(self):
""" Ensure that duplicate photos are rejected """
# Attempt to upload a duplicate
with self.assertRaises(openphoto.OpenPhotoDuplicateError):
self.client.photo.upload("tests/test_photo1.jpg",
with self.assertRaises(trovebox.TroveboxDuplicateError):
self.client.photo.upload("tests/data/test_photo1.jpg",
title=self.TEST_TITLE)
# Check there are still three photos
@ -75,7 +75,7 @@ class TestPhotos(tests.test_base.TestBase):
photo = self.photos[0]
self.assertNotEqual(photo.title, title)
# Add the title to a photo using the OpenPhoto class
# Add the title to a photo using the Trovebox class
ret_val = self.client.photo.update(photo, title=title)
# Check that it's there
@ -117,7 +117,7 @@ class TestPhotos(tests.test_base.TestBase):
self.assertFalse(hasattr(photo, "path9x9"))
self.assertFalse(hasattr(photo, "path19x19"))
# View at a particular size using the OpenPhoto class
# View at a particular size using the Trovebox class
photo = self.client.photo.view(photo, returnSizes="9x9")
self.assertTrue(hasattr(photo, "path9x9"))

View file

@ -3,11 +3,11 @@ try:
except ImportError:
import unittest
import tests.test_base
from tests.functional import test_base
@unittest.skipIf(tests.test_base.get_test_server_api() == 1,
@unittest.skipIf(test_base.get_test_server_api() == 1,
"The tag API didn't work at v1 - see frontend issue #927")
class TestTags(tests.test_base.TestBase):
class TestTags(test_base.TestBase):
testcase_name = "tag API"
def test_create_delete(self, tag_id="create_tag"):
@ -52,8 +52,8 @@ class TestTags(tests.test_base.TestBase):
"since there are no fields that can be updated")
def test_update(self):
""" Test that a tag can be updated """
# Update the tag using the OpenPhoto class, passing in the tag object
owner = "test1@openphoto.me"
# Update the tag using the Trovebox class, passing in the tag object
owner = "test1@trovebox.com"
ret_val = self.client.tag.update(self.tags[0], owner=owner)
# Check that the tag is updated
@ -61,8 +61,8 @@ class TestTags(tests.test_base.TestBase):
self.assertEqual(self.tags[0].owner, owner)
self.assertEqual(ret_val.owner, owner)
# Update the tag using the OpenPhoto class, passing in the tag id
owner = "test2@openphoto.me"
# Update the tag using the Trovebox class, passing in the tag id
owner = "test2@trovebox.com"
ret_val = self.client.tag.update(self.TEST_TAG, owner=owner)
# Check that the tag is updated
@ -71,7 +71,7 @@ class TestTags(tests.test_base.TestBase):
self.assertEqual(ret_val.owner, owner)
# Update the tag using the Tag object directly
owner = "test3@openphoto.me"
owner = "test3@trovebox.com"
ret_val = self.tags[0].update(owner=owner)
# Check that the tag is updated

0
tests/unit/__init__.py Normal file
View file

View file

@ -0,0 +1 @@
Test File

244
tests/unit/test_albums.py Normal file
View file

@ -0,0 +1,244 @@
from __future__ import unicode_literals
import mock
try:
import unittest2 as unittest # Python2.6
except ImportError:
import unittest
import trovebox
class TestAlbums(unittest.TestCase):
test_host = "test.example.com"
test_albums_dict = [{"cover": {"id": "1a", "tags": ["tag1", "tag2"]},
"id": "1",
"name": "Album 1",
"totalRows": 2},
{"cover": {"id": "2b", "tags": ["tag3", "tag4"]},
"id": "2",
"name": "Album 2",
"totalRows": 2}]
def setUp(self):
self.client = trovebox.Trovebox(host=self.test_host)
self.test_albums = [trovebox.objects.Album(self.client, album)
for album in self.test_albums_dict]
@staticmethod
def _return_value(result, message="", code=200):
return {"message": message, "code": code, "result": result}
class TestAlbumsList(TestAlbums):
@mock.patch.object(trovebox.Trovebox, 'get')
def test_albums_list(self, mock_get):
"""Check that the album list is returned correctly"""
mock_get.return_value = self._return_value(self.test_albums_dict)
result = self.client.albums.list()
mock_get.assert_called_with("/albums/list.json")
self.assertEqual(len(result), 2)
self.assertEqual(result[0].id, "1")
self.assertEqual(result[0].name, "Album 1")
self.assertEqual(result[1].id, "2")
self.assertEqual(result[1].name, "Album 2")
@mock.patch.object(trovebox.Trovebox, 'get')
def test_albums_list_returns_cover_photos(self, mock_get):
"""Check that the album list returns cover photo objects"""
mock_get.return_value = self._return_value(self.test_albums_dict)
result = self.client.albums.list()
mock_get.assert_called_with("/albums/list.json")
self.assertEqual(len(result), 2)
self.assertEqual(result[0].id, "1")
self.assertEqual(result[0].name, "Album 1")
self.assertEqual(result[0].cover.id, "1a")
self.assertEqual(result[0].cover.tags, ["tag1", "tag2"])
self.assertEqual(result[1].id, "2")
self.assertEqual(result[1].name, "Album 2")
self.assertEqual(result[1].cover.id, "2b")
self.assertEqual(result[1].cover.tags, ["tag3", "tag4"])
class TestAlbumCreate(TestAlbums):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_album_create(self, mock_post):
"""Check that an album can be created"""
mock_post.return_value = self._return_value(self.test_albums_dict[0])
result = self.client.album.create(name="Test", foo="bar")
mock_post.assert_called_with("/album/create.json", name="Test",
foo="bar")
self.assertEqual(result.id, "1")
self.assertEqual(result.name, "Album 1")
self.assertEqual(result.cover.id, "1a")
self.assertEqual(result.cover.tags, ["tag1", "tag2"])
class TestAlbumDelete(TestAlbums):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_album_delete(self, mock_post):
"""Check that an album can be deleted"""
mock_post.return_value = self._return_value(True)
result = self.client.album.delete(self.test_albums[0])
mock_post.assert_called_with("/album/1/delete.json")
self.assertEqual(result, True)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_album_delete_id(self, mock_post):
"""Check that an album can be deleted using its ID"""
mock_post.return_value = self._return_value(True)
result = self.client.album.delete("1")
mock_post.assert_called_with("/album/1/delete.json")
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')
def test_album_object_delete(self, mock_post):
"""Check that an album can be deleted using the album object directly"""
mock_post.return_value = self._return_value(True)
album = self.test_albums[0]
result = album.delete()
mock_post.assert_called_with("/album/1/delete.json")
self.assertEqual(result, True)
self.assertEqual(album.get_fields(), {})
self.assertEqual(album.id, None)
self.assertEqual(album.name, None)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_album_object_delete_failure(self, mock_post):
"""
Check that an exception is raised if an album cannot be deleted
when using the album object directly
"""
mock_post.return_value = self._return_value(False)
with self.assertRaises(trovebox.TroveboxError):
self.test_albums[0].delete()
class TestAlbumForm(TestAlbums):
@mock.patch.object(trovebox.Trovebox, 'post')
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')
def test_album_form_id(self, _):
""" If album.form gets implemented, write a test! """
with self.assertRaises(NotImplementedError):
self.client.album.form("1")
@mock.patch.object(trovebox.Trovebox, 'post')
def test_album_object_form(self, _):
""" If album.form gets implemented, write a test! """
with self.assertRaises(NotImplementedError):
self.test_albums[0].form()
class TestAlbumAddPhotos(TestAlbums):
@mock.patch.object(trovebox.Trovebox, 'post')
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')
def test_album_add_photos_id(self, _):
""" If album.add_photos gets implemented, write a test! """
with self.assertRaises(NotImplementedError):
self.client.album.add_photos("1", ["Photo Objects"])
@mock.patch.object(trovebox.Trovebox, 'post')
def test_album_object_add_photos(self, _):
""" If album.add_photos gets implemented, write a test! """
with self.assertRaises(NotImplementedError):
self.test_albums[0].add_photos(["Photo Objects"])
class TestAlbumRemovePhotos(TestAlbums):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_album_remove_photos(self, _):
""" If album.remove_photos gets implemented, write a test! """
with self.assertRaises(NotImplementedError):
self.client.album.remove_photos(self.test_albums[0],
["Photo Objects"])
@mock.patch.object(trovebox.Trovebox, 'post')
def test_album_remove_photos_id(self, _):
""" If album.remove_photos gets implemented, write a test! """
with self.assertRaises(NotImplementedError):
self.client.album.remove_photos("1", ["Photo Objects"])
@mock.patch.object(trovebox.Trovebox, 'post')
def test_album_object_remove_photos(self, _):
""" If album.remove_photos gets implemented, write a test! """
with self.assertRaises(NotImplementedError):
self.test_albums[0].remove_photos(["Photo Objects"])
class TestAlbumUpdate(TestAlbums):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_album_update(self, mock_post):
"""Check that an album can be updated"""
mock_post.return_value = self._return_value(self.test_albums_dict[1])
result = self.client.album.update(self.test_albums[0], name="Test")
mock_post.assert_called_with("/album/1/update.json", name="Test")
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_update_id(self, mock_post):
"""Check that an album can be updated using its ID"""
mock_post.return_value = self._return_value(self.test_albums_dict[1])
result = self.client.album.update("1", name="Test")
mock_post.assert_called_with("/album/1/update.json", name="Test")
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_update(self, mock_post):
"""Check that an album 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.update(name="Test")
mock_post.assert_called_with("/album/1/update.json", name="Test")
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 TestAlbumView(TestAlbums):
@mock.patch.object(trovebox.Trovebox, 'get')
def test_album_view(self, mock_get):
"""Check that an album can be viewed"""
mock_get.return_value = self._return_value(self.test_albums_dict[1])
result = self.client.album.view(self.test_albums[0], name="Test")
mock_get.assert_called_with("/album/1/view.json", name="Test")
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, 'get')
def test_album_view_id(self, mock_get):
"""Check that an album can be viewed using its ID"""
mock_get.return_value = self._return_value(self.test_albums_dict[1])
result = self.client.album.view("1", name="Test")
mock_get.assert_called_with("/album/1/view.json", name="Test")
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, 'get')
def test_album_object_view(self, mock_get):
"""Check that an album can be viewed using the album object directly"""
mock_get.return_value = self._return_value(self.test_albums_dict[1])
album = self.test_albums[0]
album.view(name="Test")
mock_get.assert_called_with("/album/1/view.json", name="Test")
self.assertEqual(album.id, "2")
self.assertEqual(album.name, "Album 2")
self.assertEqual(album.cover.id, "2b")
self.assertEqual(album.cover.tags, ["tag3", "tag4"])

129
tests/unit/test_cli.py Normal file
View file

@ -0,0 +1,129 @@
from __future__ import unicode_literals
import os
import sys
import mock
try:
import StringIO as io # Python2
except ImportError:
import io # Python3
try:
import unittest2 as unittest # Python2.6
except ImportError:
import unittest
import trovebox
from trovebox.main import main
class TestException(Exception):
pass
def raise_exception(_):
raise TestException()
class TestCli(unittest.TestCase):
test_file = os.path.join("tests", "unit", "data", "test_file.txt")
@mock.patch.object(trovebox.main.trovebox, "Trovebox")
@mock.patch('sys.stdout', new_callable=io.StringIO)
def test_defaults(self, _, mock_trovebox):
"""Check that the default behaviour is correct"""
get = mock_trovebox.return_value.get
main([])
mock_trovebox.assert_called_with(config_file=None)
get.assert_called_with("/photos/list.json", process_response=False)
@mock.patch.object(trovebox.main.trovebox, "Trovebox")
@mock.patch('sys.stdout', new_callable=io.StringIO)
def test_config(self, _, mock_trovebox):
"""Check that a config file can be specified"""
main(["--config=test"])
mock_trovebox.assert_called_with(config_file="test")
@mock.patch.object(trovebox.main.trovebox, "Trovebox")
@mock.patch('sys.stdout', new_callable=io.StringIO)
def test_get(self, mock_stdout, mock_trovebox):
"""Check that the get operation is working"""
get = mock_trovebox.return_value.get
get.return_value = "Result"
main(["-X", "GET", "-h", "test_host", "-e", "test_endpoint", "-F",
"field1=1", "-F", "field2=2"])
mock_trovebox.assert_called_with(host="test_host")
get.assert_called_with("test_endpoint", field1="1", field2="2",
process_response=False)
self.assertEqual(mock_stdout.getvalue(), "Result\n")
@mock.patch.object(trovebox.main.trovebox, "Trovebox")
@mock.patch('sys.stdout', new_callable=io.StringIO)
def test_post(self, mock_stdout, mock_trovebox):
"""Check that the post operation is working"""
post = mock_trovebox.return_value.post
post.return_value = "Result"
main(["-X", "POST", "-h", "test_host", "-e", "test_endpoint", "-F",
"field1=1", "-F", "field2=2"])
mock_trovebox.assert_called_with(host="test_host")
post.assert_called_with("test_endpoint", field1="1", field2="2",
files={}, process_response=False)
self.assertEqual(mock_stdout.getvalue(), "Result\n")
@mock.patch.object(trovebox.main.trovebox, "Trovebox")
@mock.patch('sys.stdout', new_callable=io.StringIO)
def test_post_files(self, _, mock_trovebox):
"""Check that files are posted correctly"""
post = mock_trovebox.return_value.post
main(["-X", "POST", "-F", "photo=@%s" % self.test_file])
# It's not possible to directly compare the file object,
# so check it manually
files = post.call_args[1]["files"]
self.assertEqual(list(files.keys()), ["photo"])
self.assertEqual(files["photo"].name, self.test_file)
@mock.patch.object(sys, "exit", raise_exception)
@mock.patch('sys.stderr', new_callable=io.StringIO)
def test_unknown_arg(self, mock_stderr):
"""Check that an unknown argument produces an error"""
with self.assertRaises(TestException):
main(["hello"])
self.assertIn("error: Unknown argument", mock_stderr.getvalue())
@mock.patch.object(sys, "exit", raise_exception)
@mock.patch('sys.stderr', new_callable=io.StringIO)
def test_unknown_option(self, mock_stderr):
"""Check that an unknown option produces an error"""
with self.assertRaises(TestException):
main(["--hello"])
self.assertIn("error: no such option", mock_stderr.getvalue())
@mock.patch.object(sys, "exit", raise_exception)
@mock.patch('sys.stdout', new_callable=io.StringIO)
def test_unknown_config(self, mock_stdout):
"""Check that an unknown config file produces an error"""
with self.assertRaises(TestException):
main(["--config=this_config_doesnt_exist"])
self.assertIn("No such file or directory", mock_stdout.getvalue())
self.assertIn("You must create a configuration file",
mock_stdout.getvalue())
self.assertIn("To get your credentials", mock_stdout.getvalue())
@mock.patch.object(trovebox.main.trovebox, "Trovebox")
@mock.patch('sys.stdout', new_callable=io.StringIO)
def test_verbose(self, mock_stdout, _):
"""Check that the verbose option is working"""
main(["-v"])
self.assertIn("Method: GET", mock_stdout.getvalue())
self.assertIn("Endpoint: /photos/list.json", mock_stdout.getvalue())
@mock.patch.object(trovebox.main.trovebox, "Trovebox")
@mock.patch('sys.stdout', new_callable=io.StringIO)
def test_pretty_print(self, mock_stdout, mock_trovebox):
"""Check that the pretty-print option is working"""
get = mock_trovebox.return_value.get
get.return_value = '{"test":1}'
main(["-p"])
self.assertEqual(mock_stdout.getvalue(), '{\n "test":1\n}\n')
@mock.patch('sys.stdout', new_callable=io.StringIO)
def test_version(self, mock_stdout):
"""Check that the version string is correctly printed"""
main(["--version"])
self.assertEqual(mock_stdout.getvalue(), trovebox.__version__ + "\n")

View file

@ -5,10 +5,10 @@ try:
except ImportError:
import unittest
from openphoto import OpenPhoto
from trovebox import Trovebox
CONFIG_HOME_PATH = os.path.join("tests", "config")
CONFIG_PATH = os.path.join(CONFIG_HOME_PATH, "openphoto")
CONFIG_PATH = os.path.join(CONFIG_HOME_PATH, "trovebox")
class TestConfig(unittest.TestCase):
def setUp(self):
@ -29,6 +29,7 @@ class TestConfig(unittest.TestCase):
@staticmethod
def create_config(config_file, host):
"""Create a dummy config file"""
with open(os.path.join(CONFIG_PATH, config_file), "w") as conf:
conf.write("host = %s\n" % host)
conf.write("# Comment\n\n")
@ -40,7 +41,7 @@ class TestConfig(unittest.TestCase):
def test_default_config(self):
""" Ensure the default config is loaded """
self.create_config("default", "Test Default Host")
client = OpenPhoto()
client = Trovebox()
config = client.config
self.assertEqual(client.host, "Test Default Host")
self.assertEqual(config.consumer_key, "default_consumer_key")
@ -52,7 +53,7 @@ class TestConfig(unittest.TestCase):
""" Ensure a custom config can be loaded """
self.create_config("default", "Test Default Host")
self.create_config("custom", "Test Custom Host")
client = OpenPhoto(config_file="custom")
client = Trovebox(config_file="custom")
config = client.config
self.assertEqual(client.host, "Test Custom Host")
self.assertEqual(config.consumer_key, "custom_consumer_key")
@ -64,7 +65,7 @@ class TestConfig(unittest.TestCase):
""" Ensure a full custom config path can be loaded """
self.create_config("path", "Test Path Host")
full_path = os.path.abspath(CONFIG_PATH)
client = OpenPhoto(config_file=os.path.join(full_path, "path"))
client = Trovebox(config_file=os.path.join(full_path, "path"))
config = client.config
self.assertEqual(client.host, "Test Path Host")
self.assertEqual(config.consumer_key, "path_consumer_key")
@ -75,7 +76,7 @@ class TestConfig(unittest.TestCase):
def test_host_override(self):
""" Ensure that specifying a host overrides the default config """
self.create_config("default", "Test Default Host")
client = OpenPhoto(host="host_override")
client = Trovebox(host="host_override")
config = client.config
self.assertEqual(config.host, "host_override")
self.assertEqual(config.consumer_key, "")
@ -86,14 +87,14 @@ class TestConfig(unittest.TestCase):
def test_missing_config_files(self):
""" Ensure that missing config files raise exceptions """
with self.assertRaises(IOError):
OpenPhoto()
Trovebox()
with self.assertRaises(IOError):
OpenPhoto(config_file="custom")
Trovebox(config_file="custom")
def test_host_and_config_file(self):
""" It's not valid to specify both a host and a config_file """
self.create_config("custom", "Test Custom Host")
with self.assertRaises(ValueError):
OpenPhoto(config_file="custom", host="host_override")
Trovebox(config_file="custom", host="host_override")

173
tests/unit/test_http.py Normal file
View file

@ -0,0 +1,173 @@
from __future__ import unicode_literals
import os
import json
import httpretty
try:
import unittest2 as unittest # Python2.6
except ImportError:
import unittest
import trovebox
class TestHttp(unittest.TestCase):
test_host = "test.example.com"
test_endpoint = "test.json"
test_uri = "http://%s/%s" % (test_host, test_endpoint)
test_data = {"message": "Test Message",
"code": 200,
"result": "Test Result"}
test_oauth = {"consumer_key": "dummy",
"consumer_secret": "dummy",
"token": "dummy",
"token_secret": "dummy"}
test_file = os.path.join("tests", "unit", "data", "test_file.txt")
def setUp(self):
self.client = trovebox.Trovebox(host=self.test_host,
**self.test_oauth)
def _register_uri(self, method, uri=test_uri, data=None, body=None,
**kwds):
"""Convenience wrapper around httpretty.register_uri"""
if data is None:
data = self.test_data
if body is None:
body = json.dumps(data)
httpretty.register_uri(method, uri=uri, body=body, **kwds)
@staticmethod
def _last_request():
"""This is a temporary measure until httpretty PR#59 is merged"""
return httpretty.httpretty.last_request
def test_attributes(self):
"""Check that the host attribute has been set correctly"""
self.assertEqual(self.client.host, self.test_host)
self.assertEqual(self.client.config.host, self.test_host)
@httpretty.activate
def test_get_with_parameters(self):
"""Check that the get method accepts parameters correctly"""
self._register_uri(httpretty.GET)
response = self.client.get(self.test_endpoint,
foo="bar", spam="eggs")
self.assertIn("OAuth", self._last_request().headers["authorization"])
self.assertEqual(self._last_request().querystring["foo"], ["bar"])
self.assertEqual(self._last_request().querystring["spam"], ["eggs"])
self.assertEqual(response, self.test_data)
self.assertEqual(self.client.last_url, self.test_uri)
self.assertEqual(self.client.last_params, {"foo": b"bar",
"spam": b"eggs"})
self.assertEqual(self.client.last_response.json(), self.test_data)
@httpretty.activate
def test_post_with_parameters(self):
"""Check that the post method accepts parameters correctly"""
self._register_uri(httpretty.POST)
response = self.client.post(self.test_endpoint,
foo="bar", spam="eggs")
self.assertIn(b"spam=eggs", self._last_request().body)
self.assertIn(b"foo=bar", self._last_request().body)
self.assertEqual(response, self.test_data)
self.assertEqual(self.client.last_url, self.test_uri)
self.assertEqual(self.client.last_params, {"foo": b"bar",
"spam": b"eggs"})
self.assertEqual(self.client.last_response.json(), self.test_data)
@httpretty.activate
def test_get_without_oauth(self):
"""Check that the get method works without OAuth parameters"""
self.client = trovebox.Trovebox(host=self.test_host)
self._register_uri(httpretty.GET)
response = self.client.get(self.test_endpoint)
self.assertNotIn("authorization", self._last_request().headers)
self.assertEqual(response, self.test_data)
@httpretty.activate
def test_post_without_oauth(self):
"""Check that the post method fails without OAuth parameters"""
self.client = trovebox.Trovebox(host=self.test_host)
self._register_uri(httpretty.POST)
with self.assertRaises(trovebox.TroveboxError):
self.client.post(self.test_endpoint)
@httpretty.activate
def test_get_without_response_processing(self):
"""Check that the get method works with response processing disabled"""
self._register_uri(httpretty.GET)
response = self.client.get(self.test_endpoint, process_response=False)
self.assertEqual(response, json.dumps(self.test_data))
@httpretty.activate
def test_post_without_response_processing(self):
"""Check that the post method works with response processing disabled"""
self._register_uri(httpretty.POST)
response = self.client.post(self.test_endpoint, process_response=False)
self.assertEqual(response, json.dumps(self.test_data))
@httpretty.activate
def test_get_parameter_processing(self):
"""Check that the parameter processing function is working"""
self._register_uri(httpretty.GET)
photo = trovebox.objects.Photo(None, {"id": "photo_id"})
album = trovebox.objects.Album(None, {"id": "album_id"})
tag = trovebox.objects.Tag(None, {"id": "tag_id"})
self.client.get(self.test_endpoint,
photo=photo, album=album, tag=tag,
list_=[photo, album, tag],
boolean=True,
unicode_="\xfcmlaut")
params = self._last_request().querystring
self.assertEqual(params["photo"], ["photo_id"])
self.assertEqual(params["album"], ["album_id"])
self.assertEqual(params["tag"], ["tag_id"])
self.assertEqual(params["list_"], ["photo_id,album_id,tag_id"])
self.assertEqual(params["boolean"], ["1"])
self.assertIn(params["unicode_"], [["\xc3\xbcmlaut"], ["\xfcmlaut"]])
@httpretty.activate
def test_get_with_api_version(self):
"""Check that an API version can be specified for the get method"""
self.client = trovebox.Trovebox(host=self.test_host, api_version=1)
self._register_uri(httpretty.GET,
uri="http://%s/v1/%s" % (self.test_host,
self.test_endpoint))
self.client.get(self.test_endpoint)
@httpretty.activate
def test_post_with_api_version(self):
"""Check that an API version can be specified for the post method"""
self.client = trovebox.Trovebox(host=self.test_host, api_version=1,
**self.test_oauth)
self._register_uri(httpretty.POST,
uri="http://%s/v1/%s" % (self.test_host,
self.test_endpoint))
self.client.post(self.test_endpoint)
@httpretty.activate
def test_post_file(self):
"""Check that a file can be posted"""
self._register_uri(httpretty.POST)
with open(self.test_file, 'rb') as in_file:
response = self.client.post(self.test_endpoint,
files={"file": in_file})
self.assertEqual(response, self.test_data)
body = str(self._last_request().body)
self.assertIn("Content-Disposition: form-data; "+
"name=\"file\"; filename=\"test_file.txt\"", body)
self.assertIn("Test File", str(body))
@httpretty.activate
def test_post_file_parameters_are_sent_as_querystring(self):
"""
Check that parameters are send as a query string
when a file is posted
"""
self._register_uri(httpretty.POST)
with open(self.test_file, 'rb') as in_file:
response = self.client.post(self.test_endpoint, foo="bar",
files={"file": in_file})
self.assertEqual(response, self.test_data)
self.assertEqual(self._last_request().querystring["foo"], ["bar"])

View file

@ -0,0 +1,187 @@
from __future__ import unicode_literals
import json
import httpretty
# TEMP: Temporary hack until httpretty string checking is fixed
if httpretty.compat.PY3:
httpretty.core.basestring = (bytes, str)
try:
import unittest2 as unittest # Python2.6
except ImportError:
import unittest
import trovebox
class TestHttpErrors(unittest.TestCase):
test_host = "test.example.com"
test_endpoint = "test.json"
test_uri = "http://%s/%s" % (test_host, test_endpoint)
test_data = {"message": "Test Message",
"code": 200,
"result": "Test Result"}
test_oauth = {"consumer_key": "dummy",
"consumer_secret": "dummy",
"token": "dummy",
"token_secret": "dummy"}
def setUp(self):
self.client = trovebox.Trovebox(host=self.test_host,
**self.test_oauth)
def _register_uri(self, method, uri=test_uri,
data=None, body=None, status=200, **kwds):
"""Convenience wrapper around httpretty.register_uri"""
if data is None:
data = self.test_data
# Set the JSON return code to match the HTTP status
data["code"] = status
if body is None:
body = json.dumps(data)
httpretty.register_uri(method, uri=uri, body=body, status=status,
**kwds)
@httpretty.activate
def test_get_with_error_status(self):
"""
Check that an error status causes the get method
to raise an exception
"""
self._register_uri(httpretty.GET, status=500)
with self.assertRaises(trovebox.TroveboxError):
self.client.get(self.test_endpoint)
@httpretty.activate
def test_post_with_error_status(self):
"""
Check that an error status causes the post method
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
"""
self._register_uri(httpretty.GET, status=404)
with self.assertRaises(trovebox.Trovebox404Error):
self.client.get(self.test_endpoint)
@httpretty.activate
def test_post_with_404_status(self):
"""
Check that a 404 status causes the post method
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
"""
self._register_uri(httpretty.GET, body="Invalid JSON")
with self.assertRaises(ValueError):
self.client.get(self.test_endpoint)
@httpretty.activate
def test_post_with_invalid_json(self):
"""
Check that invalid JSON causes the post method to
raise an exception
"""
self._register_uri(httpretty.POST, body="Invalid JSON")
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):
self.client.get(self.test_endpoint)
@httpretty.activate
def test_post_with_error_status_and_invalid_json(self):
"""
Check that invalid JSON causes the post method to raise an exception,
even with an error status is returned
"""
self._register_uri(httpretty.POST, body="Invalid JSON", status=500)
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):
self.client.get(self.test_endpoint)
@httpretty.activate
def test_post_with_404_status_and_invalid_json(self):
"""
Check that invalid JSON causes the post method to raise an exception,
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
"""
data = {"message": "This photo already exists", "code": 409}
self._register_uri(httpretty.GET, data=data, status=409)
with self.assertRaises(trovebox.TroveboxDuplicateError):
self.client.get(self.test_endpoint)
@httpretty.activate
def test_post_with_duplicate_status(self):
"""
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
JSON status code for get requests.
"""
data = {"message": "Test Message", "code": 202}
self._register_uri(httpretty.GET, data=data, status=200)
response = self.client.get(self.test_endpoint)
self.assertEqual(response["code"], 202)
@httpretty.activate
def test_post_with_status_code_mismatch(self):
"""
Check that a mismatched HTTP status code still returns the
JSON status code for post requests.
"""
data = {"message": "Test Message", "code": 202}
self._register_uri(httpretty.POST, data=data, status=200)
response = self.client.post(self.test_endpoint)
self.assertEqual(response["code"], 202)

429
tests/unit/test_photos.py Normal file
View file

@ -0,0 +1,429 @@
from __future__ import unicode_literals
import os
import base64
import mock
try:
import unittest2 as unittest # Python2.6
except ImportError:
import unittest
import trovebox
class TestPhotos(unittest.TestCase):
test_host = "test.example.com"
test_file = os.path.join("tests", "unit", "data", "test_file.txt")
test_photos_dict = [{"id": "1a", "tags": ["tag1", "tag2"],
"totalPages": 1, "totalRows": 2},
{"id": "2b", "tags": ["tag3", "tag4"],
"totalPages": 1, "totalRows": 2}]
def setUp(self):
self.client = trovebox.Trovebox(host=self.test_host)
self.test_photos = [trovebox.objects.Photo(self.client, photo)
for photo in self.test_photos_dict]
@staticmethod
def _return_value(result, message="", code=200):
return {"message": message, "code": code, "result": result}
class TestPhotosList(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'get')
def test_photos_list(self, mock_get):
"""Check that the photo list is returned correctly"""
mock_get.return_value = self._return_value(self.test_photos_dict)
result = self.client.photos.list()
mock_get.assert_called_with("/photos/list.json")
self.assertEqual(len(result), 2)
self.assertEqual(result[0].id, "1a")
self.assertEqual(result[0].tags, ["tag1", "tag2"])
self.assertEqual(result[1].id, "2b")
self.assertEqual(result[1].tags, ["tag3", "tag4"])
class TestPhotosUpdate(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photos_update(self, mock_post):
"""Check that multiple photos can be updated"""
mock_post.return_value = self._return_value(True)
result = self.client.photos.update(self.test_photos, title="Test")
mock_post.assert_called_with("/photos/update.json",
ids=["1a", "2b"], title="Test")
self.assertEqual(result, True)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photos_update_ids(self, mock_post):
"""Check that multiple photos can be updated using their IDs"""
mock_post.return_value = self._return_value(True)
result = self.client.photos.update(["1a", "2b"], title="Test")
mock_post.assert_called_with("/photos/update.json",
ids=["1a", "2b"], title="Test")
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):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photos_delete(self, mock_post):
"""Check that multiple photos can be deleted"""
mock_post.return_value = self._return_value(True)
result = self.client.photos.delete(self.test_photos)
mock_post.assert_called_with("/photos/delete.json", ids=["1a", "2b"])
self.assertEqual(result, True)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photos_delete_ids(self, mock_post):
"""Check that multiple photos can be deleted using their IDs"""
mock_post.return_value = self._return_value(True)
result = self.client.photos.delete(["1a", "2b"])
mock_post.assert_called_with("/photos/delete.json", ids=["1a", "2b"])
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):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_delete(self, mock_post):
"""Check that a photo can be deleted"""
mock_post.return_value = self._return_value(True)
result = self.client.photo.delete(self.test_photos[0])
mock_post.assert_called_with("/photo/1a/delete.json")
self.assertEqual(result, True)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_delete_id(self, mock_post):
"""Check that a photo can be deleted using its ID"""
mock_post.return_value = self._return_value(True)
result = self.client.photo.delete("1a")
mock_post.assert_called_with("/photo/1a/delete.json")
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')
def test_photo_object_delete(self, mock_post):
"""
Check that a photo can be deleted when using
the photo object directly
"""
mock_post.return_value = self._return_value(True)
photo = self.test_photos[0]
result = photo.delete()
mock_post.assert_called_with("/photo/1a/delete.json")
self.assertEqual(result, True)
self.assertEqual(photo.get_fields(), {})
self.assertEqual(photo.id, None)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_object_delete_failure(self, mock_post):
"""
Check that an exception is raised if a photo cannot be deleted
when using the photo object directly
"""
mock_post.return_value = self._return_value(False)
with self.assertRaises(trovebox.TroveboxError):
self.test_photos[0].delete()
class TestPhotoEdit(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'get')
def test_photo_edit(self, mock_get):
"""Check that a the photo edit endpoint is working"""
mock_get.return_value = self._return_value({"markup": "<form/>"})
result = self.client.photo.edit(self.test_photos[0])
mock_get.assert_called_with("/photo/1a/edit.json")
self.assertEqual(result, "<form/>")
@mock.patch.object(trovebox.Trovebox, 'get')
def test_photo_edit_id(self, mock_get):
"""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
when using the photo object directly
"""
mock_get.return_value = self._return_value({"markup": "<form/>"})
result = self.test_photos[0].edit()
mock_get.assert_called_with("/photo/1a/edit.json")
self.assertEqual(result, "<form/>")
class TestPhotoReplace(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_replace(self, _):
""" If photo.replace gets implemented, write a test! """
with self.assertRaises(NotImplementedError):
self.client.photo.replace(self.test_photos[0], self.test_file)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_replace_id(self, _):
""" If photo.replace gets implemented, write a test! """
with self.assertRaises(NotImplementedError):
self.client.photo.replace("1a", self.test_file)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_object_replace(self, _):
""" If photo.replace gets implemented, write a test! """
with self.assertRaises(NotImplementedError):
self.test_photos[0].replace(self.test_file)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_replace_encoded(self, _):
""" If photo.replace_encoded gets implemented, write a test! """
with self.assertRaises(NotImplementedError):
self.client.photo.replace_encoded(self.test_photos[0],
self.test_file)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_replace_encoded_id(self, _):
""" If photo.replace_encoded gets implemented, write a test! """
with self.assertRaises(NotImplementedError):
self.client.photo.replace_encoded("1a", self.test_file)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_object_replace_encoded(self, _):
""" If photo.replace_encoded gets implemented, write a test! """
with self.assertRaises(NotImplementedError):
self.test_photos[0].replace_encoded(photo_file=self.test_file)
class TestPhotoUpdate(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_update(self, mock_post):
"""Check that a photo can be updated"""
mock_post.return_value = self._return_value(self.test_photos_dict[1])
result = self.client.photo.update(self.test_photos[0], title="Test")
mock_post.assert_called_with("/photo/1a/update.json", title="Test")
self.assertEqual(result.get_fields(), self.test_photos_dict[1])
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_update_id(self, mock_post):
"""Check that a photo can be updated using its ID"""
mock_post.return_value = self._return_value(self.test_photos_dict[1])
result = self.client.photo.update("1a", title="Test")
mock_post.assert_called_with("/photo/1a/update.json", title="Test")
self.assertEqual(result.get_fields(), self.test_photos_dict[1])
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_object_update(self, mock_post):
"""
Check that a photo can be updated
when using the photo object directly
"""
mock_post.return_value = self._return_value(self.test_photos_dict[1])
photo = self.test_photos[0]
photo.update(title="Test")
mock_post.assert_called_with("/photo/1a/update.json", title="Test")
self.assertEqual(photo.get_fields(), self.test_photos_dict[1])
class TestPhotoView(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'get')
def test_photo_view(self, mock_get):
"""Check that a photo can be viewed"""
mock_get.return_value = self._return_value(self.test_photos_dict[1])
result = self.client.photo.view(self.test_photos[0],
returnSizes="20x20")
mock_get.assert_called_with("/photo/1a/view.json", returnSizes="20x20")
self.assertEqual(result.get_fields(), self.test_photos_dict[1])
@mock.patch.object(trovebox.Trovebox, 'get')
def test_photo_view_id(self, mock_get):
"""Check that a photo can be viewed using its ID"""
mock_get.return_value = self._return_value(self.test_photos_dict[1])
result = self.client.photo.view("1a", returnSizes="20x20")
mock_get.assert_called_with("/photo/1a/view.json", returnSizes="20x20")
self.assertEqual(result.get_fields(), self.test_photos_dict[1])
@mock.patch.object(trovebox.Trovebox, 'get')
def test_photo_object_view(self, mock_get):
"""
Check that a photo can be viewed
when using the photo object directly
"""
mock_get.return_value = self._return_value(self.test_photos_dict[1])
photo = self.test_photos[0]
photo.view(returnSizes="20x20")
mock_get.assert_called_with("/photo/1a/view.json", returnSizes="20x20")
self.assertEqual(photo.get_fields(), self.test_photos_dict[1])
class TestPhotoUpload(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_upload(self, mock_post):
"""Check that a photo can be uploaded"""
mock_post.return_value = self._return_value(self.test_photos_dict[0])
result = self.client.photo.upload(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/upload.json",))
self.assertEqual(title, "Test")
self.assertIn("photo", files)
self.assertEqual(result.get_fields(), self.test_photos_dict[0])
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_upload_encoded(self, mock_post):
"""Check that a photo can be uploaded using Base64 encoding"""
mock_post.return_value = self._return_value(self.test_photos_dict[0])
result = self.client.photo.upload_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/upload.json",
photo=encoded_file, title="Test")
self.assertEqual(result.get_fields(), self.test_photos_dict[0])
class TestPhotoDynamicUrl(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'get')
def test_photo_dynamic_url(self, _):
""" If photo.dynamic_url gets implemented, write a test! """
with self.assertRaises(NotImplementedError):
self.client.photo.dynamic_url(self.test_photos[0])
@mock.patch.object(trovebox.Trovebox, 'get')
def test_photo_dynamic_url_id(self, _):
""" If photo.dynamic_url gets implemented, write a test! """
with self.assertRaises(NotImplementedError):
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):
@mock.patch.object(trovebox.Trovebox, 'get')
def test_photo_next_previous(self, mock_get):
"""Check that the next/previous photos are returned"""
mock_get.return_value = self._return_value(
{"next": [self.test_photos_dict[0]],
"previous": [self.test_photos_dict[1]]})
result = self.client.photo.next_previous(self.test_photos[0])
mock_get.assert_called_with("/photo/1a/nextprevious.json")
self.assertEqual(result["next"][0].get_fields(),
self.test_photos_dict[0])
self.assertEqual(result["previous"][0].get_fields(),
self.test_photos_dict[1])
@mock.patch.object(trovebox.Trovebox, 'get')
def test_photo_next_previous_id(self, mock_get):
"""
Check that the next/previous photos are returned
when using the photo ID
"""
mock_get.return_value = self._return_value(
{"next": [self.test_photos_dict[0]],
"previous": [self.test_photos_dict[1]]})
result = self.client.photo.next_previous("1a")
mock_get.assert_called_with("/photo/1a/nextprevious.json")
self.assertEqual(result["next"][0].get_fields(),
self.test_photos_dict[0])
self.assertEqual(result["previous"][0].get_fields(),
self.test_photos_dict[1])
@mock.patch.object(trovebox.Trovebox, 'get')
def test_photo_object_next_previous(self, mock_get):
"""
Check that the next/previous photos are returned
when using the photo object directly
"""
mock_get.return_value = self._return_value(
{"next": [self.test_photos_dict[0]],
"previous": [self.test_photos_dict[1]]})
result = self.test_photos[0].next_previous()
mock_get.assert_called_with("/photo/1a/nextprevious.json")
self.assertEqual(result["next"][0].get_fields(),
self.test_photos_dict[0])
self.assertEqual(result["previous"][0].get_fields(),
self.test_photos_dict[1])
@mock.patch.object(trovebox.Trovebox, 'get')
def test_photo_next(self, mock_get):
"""Check that the next photos are returned"""
mock_get.return_value = self._return_value(
{"next": [self.test_photos_dict[0]]})
result = self.client.photo.next_previous(self.test_photos[0])
mock_get.assert_called_with("/photo/1a/nextprevious.json")
self.assertEqual(result["next"][0].get_fields(),
self.test_photos_dict[0])
self.assertNotIn("previous", result)
@mock.patch.object(trovebox.Trovebox, 'get')
def test_photo_previous(self, mock_get):
"""Check that the previous photos are returned"""
mock_get.return_value = self._return_value(
{"previous": [self.test_photos_dict[1]]})
result = self.client.photo.next_previous(self.test_photos[0])
mock_get.assert_called_with("/photo/1a/nextprevious.json")
self.assertEqual(result["previous"][0].get_fields(),
self.test_photos_dict[1])
self.assertNotIn("next", result)
@mock.patch.object(trovebox.Trovebox, 'get')
def test_photo_multiple_next_previous(self, mock_get):
"""Check that multiple next/previous photos are returned"""
mock_get.return_value = self._return_value(
{"next": [self.test_photos_dict[0], self.test_photos_dict[0]],
"previous": [self.test_photos_dict[1], self.test_photos_dict[1]]})
result = self.client.photo.next_previous(self.test_photos[0])
mock_get.assert_called_with("/photo/1a/nextprevious.json")
self.assertEqual(result["next"][0].get_fields(),
self.test_photos_dict[0])
self.assertEqual(result["next"][1].get_fields(),
self.test_photos_dict[0])
self.assertEqual(result["previous"][0].get_fields(),
self.test_photos_dict[1])
self.assertEqual(result["previous"][1].get_fields(),
self.test_photos_dict[1])
class TestPhotoTransform(TestPhotos):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_transform(self, mock_post):
"""Check that a photo can be transformed"""
mock_post.return_value = self._return_value(self.test_photos_dict[1])
result = self.client.photo.transform(self.test_photos[0], rotate="90")
mock_post.assert_called_with("/photo/1a/transform.json", rotate="90")
self.assertEqual(result.get_fields(), self.test_photos_dict[1])
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_transform_id(self, mock_post):
"""Check that a photo can be transformed using its ID"""
mock_post.return_value = self._return_value(self.test_photos_dict[1])
result = self.client.photo.transform("1a", rotate="90")
mock_post.assert_called_with("/photo/1a/transform.json", rotate="90")
self.assertEqual(result.get_fields(), self.test_photos_dict[1])
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_object_transform(self, mock_post):
"""
Check that a photo can be transformed
when using the photo object directly
"""
mock_post.return_value = self._return_value(self.test_photos_dict[1])
photo = self.test_photos[0]
photo.transform(rotate="90")
mock_post.assert_called_with("/photo/1a/transform.json", rotate="90")
self.assertEqual(photo.get_fields(), self.test_photos_dict[1])

111
tests/unit/test_tags.py Normal file
View file

@ -0,0 +1,111 @@
from __future__ import unicode_literals
import mock
try:
import unittest2 as unittest # Python2.6
except ImportError:
import unittest
import trovebox
class TestTags(unittest.TestCase):
test_host = "test.example.com"
test_tags = None
test_tags_dict = [{"count": 11, "id":"tag1"},
{"count": 5, "id":"tag2"}]
def setUp(self):
self.client = trovebox.Trovebox(host=self.test_host)
self.test_tags = [trovebox.objects.Tag(self.client, tag)
for tag in self.test_tags_dict]
@staticmethod
def _return_value(result, message="", code=200):
return {"message": message, "code": code, "result": result}
class TestTagsList(TestTags):
@mock.patch.object(trovebox.Trovebox, 'get')
def test_tags_list(self, mock_get):
"""Check that the the tag list is returned correctly"""
mock_get.return_value = self._return_value(self.test_tags_dict)
result = self.client.tags.list()
mock_get.assert_called_with("/tags/list.json")
self.assertEqual(len(result), 2)
self.assertEqual(result[0].id, "tag1")
self.assertEqual(result[0].count, 11)
self.assertEqual(result[1].id, "tag2")
self.assertEqual(result[1].count, 5)
class TestTagDelete(TestTags):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_tag_delete(self, mock_post):
"""Check that a tag can be deleted"""
mock_post.return_value = self._return_value(True)
result = self.client.tag.delete(self.test_tags[0])
mock_post.assert_called_with("/tag/tag1/delete.json")
self.assertEqual(result, True)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_tag_delete_id(self, mock_post):
"""Check that a tag can be deleted using its ID"""
mock_post.return_value = self._return_value(True)
result = self.client.tag.delete("tag1")
mock_post.assert_called_with("/tag/tag1/delete.json")
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')
def test_tag_object_delete(self, mock_post):
"""Check that a tag can be deleted when using the tag object directly"""
mock_post.return_value = self._return_value(True)
tag = self.test_tags[0]
result = tag.delete()
mock_post.assert_called_with("/tag/tag1/delete.json")
self.assertEqual(result, True)
self.assertEqual(tag.get_fields(), {})
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):
@mock.patch.object(trovebox.Trovebox, 'post')
def test_tag_update(self, mock_post):
"""Check that a tag can be updated"""
mock_post.return_value = self._return_value(self.test_tags_dict[1])
result = self.client.tag.update(self.test_tags[0], name="Test")
mock_post.assert_called_with("/tag/tag1/update.json", name="Test")
self.assertEqual(result.id, "tag2")
self.assertEqual(result.count, 5)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_tag_update_id(self, mock_post):
"""Check that a tag can be updated using its ID"""
mock_post.return_value = self._return_value(self.test_tags_dict[1])
result = self.client.tag.update("tag1", name="Test")
mock_post.assert_called_with("/tag/tag1/update.json", name="Test")
self.assertEqual(result.id, "tag2")
self.assertEqual(result.count, 5)
@mock.patch.object(trovebox.Trovebox, 'post')
def test_tag_object_update(self, mock_post):
"""Check that a tag can be updated when using the tag object directly"""
mock_post.return_value = self._return_value(self.test_tags_dict[1])
tag = self.test_tags[0]
tag.update(name="Test")
mock_post.assert_called_with("/tag/tag1/update.json", name="Test")
self.assertEqual(tag.id, "tag2")
self.assertEqual(tag.count, 5)

View file

@ -2,10 +2,15 @@
envlist = py26, py27, py33
[testenv]
commands = python -m unittest discover --catch
commands = python -m unittest discover --catch tests/unit
deps =
mock >= 1.0.0
httpretty >= 0.6.1
[testenv:py26]
commands = unit2 discover --catch
commands = unit2 discover --catch tests/unit
deps =
mock >= 1.0.0
httpretty >= 0.6.1
unittest2
discover

35
trovebox/__init__.py Normal file
View file

@ -0,0 +1,35 @@
from .http import Http
from .errors import *
from ._version import __version__
from . import api_photo
from . import api_tag
from . import api_album
LATEST_API_VERSION = 2
class Trovebox(Http):
"""
Client library for Trovebox
If no parameters are specified, config is loaded from the default
location (~/.config/trovebox/default).
The config_file parameter is used to specify an alternate config file.
If the host parameter is specified, no config file is loaded and
OAuth tokens (consumer*, token*) can optionally be specified.
All requests will include the api_version path, if specified.
This should be used to ensure that your application will continue to work
even if the Trovebox API is updated to a new revision.
"""
def __init__(self, config_file=None, host=None,
consumer_key='', consumer_secret='',
token='', token_secret='',
api_version=None):
Http.__init__(self, config_file, host,
consumer_key, consumer_secret,
token, token_secret, api_version)
self.photos = api_photo.ApiPhotos(self)
self.photo = api_photo.ApiPhoto(self)
self.tags = api_tag.ApiTags(self)
self.tag = api_tag.ApiTag(self)
self.albums = api_album.ApiAlbums(self)
self.album = api_album.ApiAlbum(self)

1
trovebox/_version.py Normal file
View file

@ -0,0 +1 @@
__version__ = "0.4"

View file

@ -1,4 +1,4 @@
from openphoto.objects import Album
from .objects import Album
class ApiAlbums:
def __init__(self, client):
@ -23,7 +23,7 @@ class ApiAlbum:
"""
Delete an album.
Returns True if successful.
Raises an OpenPhotoError if not.
Raises an TroveboxError if not.
"""
if not isinstance(album, Album):
album = Album(self._client, {"id": album})

View file

@ -1,8 +1,21 @@
import base64
from openphoto.errors import OpenPhotoError
import openphoto.openphoto_http
from openphoto.objects import Photo
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:
def __init__(self, client):
@ -11,29 +24,31 @@ class ApiPhotos:
def list(self, **kwds):
""" Returns a list of Photo objects """
photos = self._client.get("/photos/list.json", **kwds)["result"]
photos = openphoto.openphoto_http.result_to_list(photos)
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 OpenPhotoError if not.
Raises TroveboxError if not.
"""
if not self._client.post("/photos/update.json", ids=photos,
ids = extract_ids(photos)
if not self._client.post("/photos/update.json", ids=ids,
**kwds)["result"]:
raise OpenPhotoError("Update response returned False")
raise TroveboxError("Update response returned False")
return True
def delete(self, photos, **kwds):
"""
Deletes a list of photos.
Returns True if successful.
Raises OpenPhotoError if not.
Raises TroveboxError if not.
"""
if not self._client.post("/photos/delete.json", ids=photos,
ids = extract_ids(photos)
if not self._client.post("/photos/delete.json", ids=ids,
**kwds)["result"]:
raise OpenPhotoError("Delete response returned False")
raise TroveboxError("Delete response returned False")
return True
class ApiPhoto:
@ -44,7 +59,7 @@ class ApiPhoto:
"""
Delete a photo.
Returns True if successful.
Raises an OpenPhotoError if not.
Raises an TroveboxError if not.
"""
if not isinstance(photo, Photo):
photo = Photo(self._client, {"id": photo})

View file

@ -1,4 +1,4 @@
from openphoto.objects import Tag
from .objects import Tag
class ApiTags:
def __init__(self, client):
@ -24,7 +24,7 @@ class ApiTag:
"""
Delete a tag.
Returns True if successful.
Raises an OpenPhotoError if not.
Raises an TroveboxError if not.
"""
if not isinstance(tag, Tag):
tag = Tag(self._client, {"id": tag})

View file

@ -41,7 +41,7 @@ def get_config_path(config_file):
config_path = os.path.join(os.getenv('HOME'), ".config")
if not config_file:
config_file = "default"
return os.path.join(config_path, "openphoto", config_file)
return os.path.join(config_path, "trovebox", config_file)
def read_config(config_path):
"""

View file

@ -1,12 +1,12 @@
class OpenPhotoError(Exception):
""" Indicates that an OpenPhoto operation failed """
class TroveboxError(Exception):
""" Indicates that an Trovebox operation failed """
pass
class OpenPhotoDuplicateError(OpenPhotoError):
class TroveboxDuplicateError(TroveboxError):
""" Indicates that an upload operation failed due to a duplicate photo """
pass
class OpenPhoto404Error(Exception):
class Trovebox404Error(Exception):
"""
Indicates that an Http 404 error code was received
(resource not found)

View file

@ -8,40 +8,36 @@ try:
except ImportError:
from urlparse import urlunparse # Python2
from openphoto.objects import OpenPhotoObject
from openphoto.errors import *
from openphoto.config import Config
from .objects import TroveboxObject
from .errors import *
from .config import Config
if sys.version < '3':
TEXT_TYPE = unicode
# requests_oauth needs to decode to ascii for Python2
OAUTH_DECODING = "utf-8"
else:
TEXT_TYPE = str
# requests_oauth needs to use (unicode) strings for Python3
OAUTH_DECODING = None
DUPLICATE_RESPONSE = {"code": 409,
"message": "This photo already exists"}
class OpenPhotoHttp:
class Http:
"""
Base class to handle HTTP requests to an OpenPhoto server.
Base class to handle HTTP requests to an Trovebox server.
If no parameters are specified, config is loaded from the default
location (~/.config/openphoto/default).
location (~/.config/trovebox/default).
The config_file parameter is used to specify an alternate config file.
If the host parameter is specified, no config file is loaded and
OAuth tokens (consumer*, token*) can optionally be specified.
All requests will include the api_version path, if specified.
This should be used to ensure that your application will continue to work
even if the OpenPhoto API is updated to a new revision.
even if the Trovebox API is updated to a new revision.
"""
def __init__(self, config_file=None, host=None,
consumer_key='', consumer_secret='',
token='', token_secret='', api_version=None):
self._api_version = api_version
self._logger = logging.getLogger("openphoto")
self._logger = logging.getLogger("trovebox")
self.config = Config(config_file, host,
consumer_key, consumer_secret,
@ -59,7 +55,7 @@ class OpenPhotoHttp:
Performs an HTTP GET from the specified endpoint (API path),
passing parameters if given.
The api_version is prepended to the endpoint,
if it was specified when the OpenPhoto object was created.
if it was specified when the Trovebox object was created.
Returns the decoded JSON dictionary, and raises exceptions if an
error code is received.
@ -76,8 +72,7 @@ class OpenPhotoHttp:
auth = requests_oauthlib.OAuth1(self.config.consumer_key,
self.config.consumer_secret,
self.config.token,
self.config.token_secret,
decoding=OAUTH_DECODING)
self.config.token_secret)
else:
auth = None
@ -103,7 +98,7 @@ class OpenPhotoHttp:
Performs an HTTP POST to the specified endpoint (API path),
passing parameters if given.
The api_version is prepended to the endpoint,
if it was specified when the OpenPhoto object was created.
if it was specified when the Trovebox object was created.
Returns the decoded JSON dictionary, and raises exceptions if an
error code is received.
@ -117,13 +112,12 @@ class OpenPhotoHttp:
url = urlunparse(('http', self.host, endpoint, '', '', ''))
if not self.config.consumer_key:
raise OpenPhotoError("Cannot issue POST without OAuth tokens")
raise TroveboxError("Cannot issue POST without OAuth tokens")
auth = requests_oauthlib.OAuth1(self.config.consumer_key,
self.config.consumer_secret,
self.config.token,
self.config.token_secret,
decoding=OAUTH_DECODING)
self.config.token_secret)
with requests.Session() as session:
if files:
# Need to pass parameters as URL query, so they get OAuth signed
@ -158,7 +152,7 @@ class OpenPhotoHttp:
processed_params = {}
for key, value in params.items():
# Extract IDs from objects
if isinstance(value, OpenPhotoObject):
if isinstance(value, TroveboxObject):
value = value.id
# Ensure value is UTF-8 encoded
@ -171,7 +165,7 @@ class OpenPhotoHttp:
new_list = list(value)
# Extract IDs from objects in the list
for i, item in enumerate(new_list):
if isinstance(item, OpenPhotoObject):
if isinstance(item, TroveboxObject):
new_list[i] = item.id
# Convert list to string
value = ','.join([str(item) for item in new_list])
@ -189,29 +183,29 @@ class OpenPhotoHttp:
Decodes the JSON response, returning a dict.
Raises an exception if an invalid response code is received.
"""
if response.status_code == 404:
raise Trovebox404Error("HTTP Error %d: %s" %
(response.status_code, response.reason))
try:
json_response = response.json()
code = json_response["code"]
message = json_response["message"]
except (ValueError, KeyError):
# Response wasn't OpenPhoto JSON - check the HTTP status code
# Response wasn't Trovebox JSON - check the HTTP status code
if 200 <= response.status_code < 300:
# Status code was valid, so just reraise the exception
raise
elif response.status_code == 404:
raise OpenPhoto404Error("HTTP Error %d: %s" %
(response.status_code, response.reason))
else:
raise OpenPhotoError("HTTP Error %d: %s" %
raise TroveboxError("HTTP Error %d: %s" %
(response.status_code, response.reason))
if 200 <= code < 300:
return json_response
elif (code == DUPLICATE_RESPONSE["code"] and
DUPLICATE_RESPONSE["message"] in message):
raise OpenPhotoDuplicateError("Code %d: %s" % (code, message))
raise TroveboxDuplicateError("Code %d: %s" % (code, message))
else:
raise OpenPhotoError("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 """

View file

@ -4,7 +4,7 @@ import sys
import json
from optparse import OptionParser
from openphoto import OpenPhoto
import trovebox
CONFIG_ERROR = """
You must create a configuration file with the following contents:
@ -29,7 +29,7 @@ def main(args=sys.argv[1:]):
parser.add_option('-c', '--config', help="Configuration file to use",
action='store', type='string', dest='config_file')
parser.add_option('-h', '-H', '--host',
help=("Hostname of the OpenPhoto server "
help=("Hostname of the Trovebox server "
"(overrides config_file)"),
action='store', type='string', dest='host')
parser.add_option('-X', help="Method to use (GET or POST)",
@ -44,6 +44,8 @@ def main(args=sys.argv[1:]):
action="store_true", dest="pretty", default=False)
parser.add_option('-v', help="Verbose output",
action="store_true", dest="verbose", default=False)
parser.add_option('--version', help="Display the current version information",
action="store_true")
parser.add_option('--help', help='show this help message',
action="store_true")
@ -53,6 +55,10 @@ def main(args=sys.argv[1:]):
parser.print_help()
return
if options.version:
print(trovebox.__version__)
return
if args:
parser.error("Unknown argument: %s" % args)
@ -64,10 +70,10 @@ def main(args=sys.argv[1:]):
# Host option overrides config file settings
if options.host:
client = OpenPhoto(host=options.host)
client = trovebox.Trovebox(host=options.host)
else:
try:
client = OpenPhoto(config_file=options.config_file)
client = trovebox.Trovebox(config_file=options.config_file)
except IOError as error:
print(error)
print(CONFIG_ERROR)
@ -81,6 +87,8 @@ def main(args=sys.argv[1:]):
params, files = extract_files(params)
result = client.post(options.endpoint, process_response=False,
files=files, **params)
for f in files:
files[f].close()
if options.verbose:
print("==========\nMethod: %s\nHost: %s\nEndpoint: %s" %
@ -100,7 +108,7 @@ def main(args=sys.argv[1:]):
def extract_files(params):
"""
Extract filenames from the "photo" parameter, so they can be uploaded, returning (updated_params, files).
Uses the same technique as openphoto-php:
Uses the same technique as the Trovebox PHP commandline tool:
* Filename can only be in the "photo" parameter
* Filename must be prefixed with "@"
* Filename must exist

View file

@ -3,12 +3,14 @@ try:
except ImportError:
from urllib import quote # Python2
class OpenPhotoObject:
from .errors import TroveboxError
class TroveboxObject:
""" Base object supporting the storage of custom fields as attributes """
def __init__(self, openphoto, json_dict):
def __init__(self, trovebox, json_dict):
self.id = None
self.name = None
self._openphoto = openphoto
self._trovebox = trovebox
self._json_dict = json_dict
self._set_fields(json_dict)
@ -29,6 +31,16 @@ class OpenPhotoObject:
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():
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)
@ -42,33 +54,35 @@ class OpenPhotoObject:
return self._json_dict
class Photo(OpenPhotoObject):
class Photo(TroveboxObject):
def delete(self, **kwds):
"""
Delete this photo.
Returns True if successful.
Raises an OpenPhotoError if not.
Raises an TroveboxError if not.
"""
result = self._openphoto.post("/photo/%s/delete.json" %
result = self._trovebox.post("/photo/%s/delete.json" %
self.id, **kwds)["result"]
self._replace_fields({})
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._openphoto.get("/photo/%s/edit.json" %
result = self._trovebox.get("/photo/%s/edit.json" %
self.id, **kwds)["result"]
return result["markup"]
def replace(self, photo_file, **kwds):
raise NotImplementedError()
def replace_encoded(self, encoded_photo, **kwds):
def replace_encoded(self, photo_file, **kwds):
raise NotImplementedError()
def update(self, **kwds):
""" Update this photo with the specified parameters """
new_dict = self._openphoto.post("/photo/%s/update.json" %
new_dict = self._trovebox.post("/photo/%s/update.json" %
self.id, **kwds)["result"]
self._replace_fields(new_dict)
@ -77,7 +91,7 @@ class Photo(OpenPhotoObject):
Used to view the photo at a particular size.
Updates the photo's fields with the response.
"""
new_dict = self._openphoto.get("/photo/%s/view.json" %
new_dict = self._trovebox.get("/photo/%s/view.json" %
self.id, **kwds)["result"]
self._replace_fields(new_dict)
@ -89,7 +103,7 @@ class Photo(OpenPhotoObject):
Returns a dict containing the next and previous photo lists
(there may be more than one next/previous photo returned).
"""
result = self._openphoto.get("/photo/%s/nextprevious.json" %
result = self._trovebox.get("/photo/%s/nextprevious.json" %
self.id, **kwds)["result"]
value = {}
if "next" in result:
@ -99,7 +113,7 @@ class Photo(OpenPhotoObject):
value["next"] = []
for photo in result["next"]:
value["next"].append(Photo(self._openphoto, photo))
value["next"].append(Photo(self._trovebox, photo))
if "previous" in result:
# Workaround for APIv1
@ -108,7 +122,7 @@ class Photo(OpenPhotoObject):
value["previous"] = []
for photo in result["previous"]:
value["previous"].append(Photo(self._openphoto, photo))
value["previous"].append(Photo(self._trovebox, photo))
return value
@ -117,81 +131,85 @@ class Photo(OpenPhotoObject):
Performs transformation specified in **kwds
Example: transform(rotate=90)
"""
new_dict = self._openphoto.post("/photo/%s/transform.json" %
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._openphoto.get("/photo/%s/view.json" %
new_dict = self._trovebox.get("/photo/%s/view.json" %
self.id)["result"]
self._replace_fields(new_dict)
class Tag(OpenPhotoObject):
class Tag(TroveboxObject):
def delete(self, **kwds):
"""
Delete this tag.
Returns True if successful.
Raises an OpenPhotoError if not.
Raises an TroveboxError if not.
"""
result = self._openphoto.post("/tag/%s/delete.json" %
result = self._trovebox.post("/tag/%s/delete.json" %
quote(self.id), **kwds)["result"]
self._replace_fields({})
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._openphoto.post("/tag/%s/update.json" % quote(self.id),
new_dict = self._trovebox.post("/tag/%s/update.json" % quote(self.id),
**kwds)["result"]
self._replace_fields(new_dict)
class Album(OpenPhotoObject):
def __init__(self, openphoto, json_dict):
OpenPhotoObject.__init__(self, openphoto, json_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._openphoto, self.cover)
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._openphoto, photo)
self.photos[i] = Photo(self._trovebox, photo)
def delete(self, **kwds):
"""
Delete this album.
Returns True if successful.
Raises an OpenPhotoError if not.
Raises an TroveboxError if not.
"""
result = self._openphoto.post("/album/%s/delete.json" %
result = self._trovebox.post("/album/%s/delete.json" %
self.id, **kwds)["result"]
self._replace_fields({})
if not result:
raise TroveboxError("Delete response returned False")
self._delete_fields()
return result
def form(self, **kwds):
raise NotImplementedError()
def add_photos(self, **kwds):
def add_photos(self, photos, **kwds):
raise NotImplementedError()
def remove_photos(self, **kwds):
def remove_photos(self, photos, **kwds):
raise NotImplementedError()
def update(self, **kwds):
""" Update this album with the specified parameters """
new_dict = self._openphoto.post("/album/%s/update.json" %
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._openphoto.get("/album/%s/view.json" %
new_dict = self._trovebox.get("/album/%s/view.json" %
self.id)["result"]
self._replace_fields(new_dict)
@ -202,7 +220,7 @@ class Album(OpenPhotoObject):
Requests the full contents of the album.
Updates the album's fields with the response.
"""
result = self._openphoto.get("/album/%s/view.json" %
result = self._trovebox.get("/album/%s/view.json" %
self.id, **kwds)["result"]
self._replace_fields(result)
self._update_fields_with_objects()