Merge branch 'release-0.6.2'
This commit is contained in:
commit
209a1da27c
17 changed files with 212 additions and 90 deletions
|
@ -2,6 +2,14 @@
|
|||
Trovebox Python Library Changelog
|
||||
=================================
|
||||
|
||||
v0.6.2
|
||||
======
|
||||
* Support Unicode tags (#74, #77)
|
||||
* Ensure lists inside parameters are UTF-8 encoded (#74, #77)
|
||||
* Fix repr unicode handling (#75)
|
||||
* Support unicode filenames (#72, #73)
|
||||
* Add Pypy to unit testing list (#78)
|
||||
|
||||
v0.6.1
|
||||
======
|
||||
* Perform user expansion when uploading files from the CLI (#59, #70)
|
||||
|
|
BIN
tests/data/test_ünicode_photo.jpg
Normal file
BIN
tests/data/test_ünicode_photo.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
|
@ -121,6 +121,27 @@ class TestPhotos(test_base.TestBase):
|
|||
photos[1].delete()
|
||||
self.photos[0].update(permission=False)
|
||||
|
||||
# Unicode filename upload not working due to frontend bug 1433
|
||||
@unittest.expectedFailure
|
||||
def test_upload_unicode_filename(self):
|
||||
"""Test that a photo with a unicode filename can be uploaded"""
|
||||
ret_val = self.client.photo.upload(u"tests/data/test_\xfcnicode_photo.jpg",
|
||||
title=self.TEST_TITLE)
|
||||
# Check that there are now four photos
|
||||
self.photos = self.client.photos.list()
|
||||
self.assertEqual(len(self.photos), 4)
|
||||
|
||||
# Check that the upload return value was correct
|
||||
pathOriginals = [photo.pathOriginal for photo in self.photos]
|
||||
self.assertIn(ret_val.pathOriginal, pathOriginals)
|
||||
|
||||
# Delete the photo
|
||||
ret_val.delete()
|
||||
|
||||
# Check that it's gone
|
||||
self.photos = self.client.photos.list()
|
||||
self.assertEqual(len(self.photos), 3)
|
||||
|
||||
def test_update(self):
|
||||
""" Update a photo by editing the title """
|
||||
title = "\xfcmlaut" # umlauted umlaut
|
||||
|
|
1
tests/unit/data/ünicode_test_file.txt
Normal file
1
tests/unit/data/ünicode_test_file.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Test File
|
|
@ -70,15 +70,15 @@ class TestActivitiesList(TestActivities):
|
|||
|
||||
@mock.patch.object(trovebox.Trovebox, 'get')
|
||||
def test_options(self, mock_get):
|
||||
"""Check that the activity list optionss are applied properly"""
|
||||
"""Check that the activity list options are applied properly"""
|
||||
mock_get.return_value = self._return_value(self.test_activities_dict)
|
||||
self.client.activities.list(options={"foo": "bar",
|
||||
"test1": "test2"},
|
||||
"test1": "\xfcmlaut"},
|
||||
foo="bar")
|
||||
# Dict element can be any order
|
||||
self.assertIn(mock_get.call_args[0],
|
||||
[("/activities/foo-bar/test1-test2/list.json",),
|
||||
("/activities/test1-test2/foo-bar/list.json",)])
|
||||
[("/activities/foo-bar/test1-%C3%BCmlaut/list.json",),
|
||||
("/activities/test1-%C3%BCmlaut/foo-bar/list.json",)])
|
||||
self.assertEqual(mock_get.call_args[1], {"foo": "bar"})
|
||||
|
||||
class TestActivitiesPurge(TestActivities):
|
||||
|
|
|
@ -23,6 +23,8 @@ def raise_exception(_):
|
|||
|
||||
class TestCli(unittest.TestCase):
|
||||
test_file = os.path.join("tests", "unit", "data", "test_file.txt")
|
||||
test_unicode_file = os.path.join("tests", "unit", "data",
|
||||
"\xfcnicode_test_file.txt")
|
||||
|
||||
@mock.patch.object(trovebox.main.trovebox, "Trovebox")
|
||||
@mock.patch('sys.stdout', new_callable=io.StringIO)
|
||||
|
@ -107,6 +109,45 @@ class TestCli(unittest.TestCase):
|
|||
with self.assertRaises(IOError):
|
||||
main(["-X", "POST", "-F", "photo=@%s.missing" % self.test_file])
|
||||
|
||||
@mock.patch.object(trovebox.main.trovebox, "Trovebox")
|
||||
@mock.patch('sys.stdout', new_callable=io.StringIO)
|
||||
def test_post_unicode_files(self, _, mock_trovebox):
|
||||
"""Check that unicode filenames are posted correctly"""
|
||||
post = mock_trovebox.return_value.post
|
||||
|
||||
# Python 2.x provides encoded commandline arguments
|
||||
file_param = "photo=@%s" % self.test_unicode_file
|
||||
if sys.version < '3':
|
||||
file_param = file_param.encode(sys.getfilesystemencoding())
|
||||
|
||||
main(["-X", "POST", "-F", "photo=@%s" % self.test_unicode_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_unicode_file)
|
||||
|
||||
@unittest.skipIf(sys.version >= '3',
|
||||
"Python3 only uses unicode commandline arguments")
|
||||
@mock.patch('trovebox.main.sys.getfilesystemencoding')
|
||||
@mock.patch.object(trovebox.main.trovebox, "Trovebox")
|
||||
@mock.patch('sys.stdout', new_callable=io.StringIO)
|
||||
def test_post_utf8_files(self, _, mock_trovebox, mock_getfilesystemencoding):
|
||||
"""Check that utf-8 encoded filenames are posted correctly"""
|
||||
post = mock_trovebox.return_value.post
|
||||
# Make the system think its filesystemencoding is utf-8
|
||||
mock_getfilesystemencoding.return_value = "utf-8"
|
||||
|
||||
file_param = "photo=@%s" % self.test_unicode_file
|
||||
file_param = file_param.encode("utf-8")
|
||||
|
||||
main(["-X", "POST", "-F", file_param])
|
||||
# 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_unicode_file)
|
||||
|
||||
@mock.patch.object(sys, "exit", raise_exception)
|
||||
@mock.patch('sys.stderr', new_callable=io.StringIO)
|
||||
def test_unknown_arg(self, mock_stderr):
|
||||
|
|
|
@ -193,7 +193,8 @@ class TestHttp(unittest.TestCase):
|
|||
self.client.get(self.test_endpoint,
|
||||
photo=photo, album=album, tag=tag,
|
||||
list_=[photo, album, tag],
|
||||
list2=["1", "2", "3"],
|
||||
list2=["1", False, 3],
|
||||
unicode_list=["1", "2", "\xfcmlaut"],
|
||||
boolean=True,
|
||||
unicode_="\xfcmlaut")
|
||||
params = self._last_request().querystring
|
||||
|
@ -201,7 +202,8 @@ class TestHttp(unittest.TestCase):
|
|||
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["list2"], ["1,2,3"])
|
||||
self.assertEqual(params["list2"], ["1,0,3"])
|
||||
self.assertIn(params["unicode_list"], [["1,2,\xc3\xbcmlaut"], ["1,2,\xfcmlaut"]])
|
||||
self.assertEqual(params["boolean"], ["1"])
|
||||
self.assertIn(params["unicode_"], [["\xc3\xbcmlaut"], ["\xfcmlaut"]])
|
||||
|
||||
|
|
|
@ -57,27 +57,27 @@ class TestPhotosList(TestPhotos):
|
|||
|
||||
@mock.patch.object(trovebox.Trovebox, 'get')
|
||||
def test_options(self, mock_get):
|
||||
"""Check that the activity list options are applied properly"""
|
||||
"""Check that the photo list options are applied properly"""
|
||||
mock_get.return_value = self._return_value(self.test_photos_dict)
|
||||
self.client.photos.list(options={"foo": "bar",
|
||||
"test1": "test2"},
|
||||
"test1": "\xfcmlaut"},
|
||||
foo="bar")
|
||||
# Dict element can be any order
|
||||
self.assertIn(mock_get.call_args[0],
|
||||
[("/photos/foo-bar/test1-test2/list.json",),
|
||||
("/photos/test1-test2/foo-bar/list.json",)])
|
||||
[("/photos/foo-bar/test1-%C3%BCmlaut/list.json",),
|
||||
("/photos/test1-%C3%BCmlaut/foo-bar/list.json",)])
|
||||
self.assertEqual(mock_get.call_args[1], {"foo": "bar"})
|
||||
|
||||
class TestPhotosShare(TestPhotos):
|
||||
@mock.patch.object(trovebox.Trovebox, 'post')
|
||||
def test_photos_share(self, mock_post):
|
||||
self.client.photos.share(options={"foo": "bar",
|
||||
"test1": "test2"},
|
||||
"test1": "\xfcmlaut"},
|
||||
foo="bar")
|
||||
# Dict element can be any order
|
||||
self.assertIn(mock_post.call_args[0],
|
||||
[("/photos/foo-bar/test1-test2/share.json",),
|
||||
("/photos/test1-test2/foo-bar/share.json",)])
|
||||
[("/photos/foo-bar/test1-%C3%BCmlaut/share.json",),
|
||||
("/photos/test1-%C3%BCmlaut/foo-bar/share.json",)])
|
||||
self.assertEqual(mock_post.call_args[1], {"foo": "bar"})
|
||||
|
||||
class TestPhotosUpdate(TestPhotos):
|
||||
|
@ -363,12 +363,12 @@ class TestPhotoView(TestPhotos):
|
|||
mock_get.return_value = self._return_value(self.test_photos_dict[1])
|
||||
result = self.client.photo.view(self.test_photos[0],
|
||||
options={"foo": "bar",
|
||||
"test1": "test2"},
|
||||
"test1": "\xfcmlaut"},
|
||||
returnSizes="20x20")
|
||||
# Dict elemet can be in any order
|
||||
self.assertIn(mock_get.call_args[0],
|
||||
[("/photo/1a/foo-bar/test1-test2/view.json",),
|
||||
("/photo/1a/test1-test2/foo-bar/view.json",)])
|
||||
[("/photo/1a/foo-bar/test1-%C3%BCmlaut/view.json",),
|
||||
("/photo/1a/test1-%C3%BCmlaut/foo-bar/view.json",)])
|
||||
self.assertEqual(mock_get.call_args[1], {"returnSizes": "20x20"})
|
||||
self.assertEqual(result.get_fields(), self.test_photos_dict[1])
|
||||
|
||||
|
@ -378,13 +378,13 @@ class TestPhotoView(TestPhotos):
|
|||
mock_get.return_value = self._return_value(self.test_photos_dict[1])
|
||||
result = self.client.photo.view("1a",
|
||||
options={"foo": "bar",
|
||||
"test1": "test2"},
|
||||
"test1": "\xfcmlaut"},
|
||||
returnSizes="20x20")
|
||||
|
||||
# Dict elemet can be in any order
|
||||
self.assertIn(mock_get.call_args[0],
|
||||
[("/photo/1a/foo-bar/test1-test2/view.json",),
|
||||
("/photo/1a/test1-test2/foo-bar/view.json",)])
|
||||
[("/photo/1a/foo-bar/test1-%C3%BCmlaut/view.json",),
|
||||
("/photo/1a/test1-%C3%BCmlaut/foo-bar/view.json",)])
|
||||
self.assertEqual(mock_get.call_args[1], {"returnSizes": "20x20"})
|
||||
self.assertEqual(result.get_fields(), self.test_photos_dict[1])
|
||||
|
||||
|
@ -419,7 +419,7 @@ class TestPhotoUpload(TestPhotos):
|
|||
files = mock_post.call_args[1]["files"]
|
||||
self.assertEqual(endpoint, ("/photo/upload.json",))
|
||||
self.assertEqual(title, "Test")
|
||||
self.assertIn("photo", files)
|
||||
self.assertEqual(files["photo"].name, self.test_file)
|
||||
self.assertEqual(result.get_fields(), self.test_photos_dict[0])
|
||||
|
||||
class TestPhotoUploadEncoded(TestPhotos):
|
||||
|
@ -455,12 +455,12 @@ class TestPhotoNextPrevious(TestPhotos):
|
|||
"previous": [self.test_photos_dict[1]]})
|
||||
result = self.client.photo.next_previous(self.test_photos[0],
|
||||
options={"foo": "bar",
|
||||
"test1": "test2"},
|
||||
"test1": "\xfcmlaut"},
|
||||
foo="bar")
|
||||
# Dict elemet can be in any order
|
||||
self.assertIn(mock_get.call_args[0],
|
||||
[("/photo/1a/nextprevious/foo-bar/test1-test2.json",),
|
||||
("/photo/1a/nextprevious/test1-test2/foo-bar.json",)])
|
||||
[("/photo/1a/nextprevious/foo-bar/test1-%C3%BCmlaut.json",),
|
||||
("/photo/1a/nextprevious/test1-%C3%BCmlaut/foo-bar.json",)])
|
||||
self.assertEqual(mock_get.call_args[1], {"foo": "bar"})
|
||||
self.assertEqual(result["next"][0].get_fields(),
|
||||
self.test_photos_dict[0])
|
||||
|
@ -478,12 +478,12 @@ class TestPhotoNextPrevious(TestPhotos):
|
|||
"previous": [self.test_photos_dict[1]]})
|
||||
result = self.client.photo.next_previous("1a",
|
||||
options={"foo": "bar",
|
||||
"test1": "test2"},
|
||||
"test1": "\xfcmlaut"},
|
||||
foo="bar")
|
||||
# Dict elemet can be in any order
|
||||
self.assertIn(mock_get.call_args[0],
|
||||
[("/photo/1a/nextprevious/foo-bar/test1-test2.json",),
|
||||
("/photo/1a/nextprevious/test1-test2/foo-bar.json",)])
|
||||
[("/photo/1a/nextprevious/foo-bar/test1-%C3%BCmlaut.json",),
|
||||
("/photo/1a/nextprevious/test1-%C3%BCmlaut/foo-bar.json",)])
|
||||
self.assertEqual(mock_get.call_args[1], {"foo": "bar"})
|
||||
self.assertEqual(result["next"][0].get_fields(),
|
||||
self.test_photos_dict[0])
|
||||
|
@ -500,12 +500,12 @@ class TestPhotoNextPrevious(TestPhotos):
|
|||
{"next": [self.test_photos_dict[0]],
|
||||
"previous": [self.test_photos_dict[1]]})
|
||||
result = self.test_photos[0].next_previous(options={"foo": "bar",
|
||||
"test1": "test2"},
|
||||
"test1": "\xfcmlaut"},
|
||||
foo="bar")
|
||||
# Dict elemet can be in any order
|
||||
self.assertIn(mock_get.call_args[0],
|
||||
[("/photo/1a/nextprevious/foo-bar/test1-test2.json",),
|
||||
("/photo/1a/nextprevious/test1-test2/foo-bar.json",)])
|
||||
[("/photo/1a/nextprevious/foo-bar/test1-%C3%BCmlaut.json",),
|
||||
("/photo/1a/nextprevious/test1-%C3%BCmlaut/foo-bar.json",)])
|
||||
self.assertEqual(mock_get.call_args[1], {"foo": "bar"})
|
||||
self.assertEqual(result["next"][0].get_fields(),
|
||||
self.test_photos_dict[0])
|
||||
|
@ -606,6 +606,11 @@ class TestPhotoObject(TestPhotos):
|
|||
"name": "Test Name"})
|
||||
self.assertEqual(repr(photo), "<Photo name='Test Name'>")
|
||||
|
||||
def test_photo_object_repr_with_unicode_id(self):
|
||||
""" Ensure that a unicode id is correctly represented """
|
||||
photo = trovebox.objects.photo.Photo(self.client, {"id": "\xfcmlaut"})
|
||||
self.assertIn(repr(photo), [b"<Photo id='\xc3\xbcmlaut'>", "<Photo id='\xfcmlaut'>"])
|
||||
|
||||
@mock.patch.object(trovebox.Trovebox, 'post')
|
||||
def test_photo_object_create_attribute(self, _):
|
||||
"""
|
||||
|
|
|
@ -10,13 +10,17 @@ 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"}]
|
||||
test_tags_dict = [{"count": 11, "id": "tag1"},
|
||||
{"count": 5, "id": "tag2"}]
|
||||
|
||||
test_tag_unicode_dict = {"id": "\xfcmlaut"}
|
||||
|
||||
def setUp(self):
|
||||
self.client = trovebox.Trovebox(host=self.test_host)
|
||||
self.test_tags = [trovebox.objects.tag.Tag(self.client, tag)
|
||||
for tag in self.test_tags_dict]
|
||||
self.test_tag_unicode = trovebox.objects.tag.Tag(self.client,
|
||||
self.test_tag_unicode_dict)
|
||||
|
||||
@staticmethod
|
||||
def _return_value(result, message="", code=200):
|
||||
|
@ -89,6 +93,14 @@ class TestTagDelete(TestTags):
|
|||
self.assertEqual(tag.get_fields(), {})
|
||||
self.assertEqual(tag.id, None)
|
||||
|
||||
@mock.patch.object(trovebox.Trovebox, 'post')
|
||||
def test_tag_object_delete_unicode(self, mock_post):
|
||||
"""Check that a unicode tag can be deleted using its ID"""
|
||||
mock_post.return_value = self._return_value(True)
|
||||
result = self.client.tag.delete(self.test_tag_unicode)
|
||||
mock_post.assert_called_with("/tag/%C3%BCmlaut/delete.json")
|
||||
self.assertEqual(result, True)
|
||||
|
||||
class TestTagUpdate(TestTags):
|
||||
@mock.patch.object(trovebox.Trovebox, 'post')
|
||||
def test_tag_update(self, mock_post):
|
||||
|
@ -118,3 +130,11 @@ class TestTagUpdate(TestTags):
|
|||
self.assertEqual(tag.id, "tag2")
|
||||
self.assertEqual(tag.count, 5)
|
||||
|
||||
@mock.patch.object(trovebox.Trovebox, 'post')
|
||||
def test_tag_object_update_unicode(self, mock_post):
|
||||
"""Check that a unicode tag can be updated using its ID"""
|
||||
mock_post.return_value = self._return_value(self.test_tag_unicode_dict)
|
||||
result = self.client.tag.update(self.test_tag_unicode, name="Test")
|
||||
mock_post.assert_called_with("/tag/%C3%BCmlaut/update.json", name="Test")
|
||||
self.assertEqual(result.id, "\xfcmlaut")
|
||||
|
||||
|
|
2
tox.ini
2
tox.ini
|
@ -1,5 +1,5 @@
|
|||
[tox]
|
||||
envlist = py26, py27, py33, coverage
|
||||
envlist = py26, py27, py33, pypy, coverage
|
||||
|
||||
[testenv]
|
||||
commands = python -m unittest discover tests/unit
|
||||
|
|
|
@ -25,9 +25,15 @@ diff --unified --recursive '--exclude=.pylint-disable.patch' original/api/api_al
|
|||
diff --unified --recursive '--exclude=.pylint-disable.patch' original/api/api_base.py patched/api/api_base.py
|
||||
--- original/api/api_base.py
|
||||
+++ patched/api/api_base.py
|
||||
@@ -2,7 +2,7 @@
|
||||
@@ -2,12 +2,12 @@
|
||||
api_base.py: Base class for all API classes
|
||||
"""
|
||||
try:
|
||||
- from urllib.parse import quote # Python3
|
||||
+ from urllib.parse import quote # Python3 # pylint: disable=import-error,no-name-in-module
|
||||
except ImportError:
|
||||
from urllib import quote # Python2
|
||||
|
||||
|
||||
-class ApiBase(object):
|
||||
+class ApiBase(object): # pylint: disable=too-few-public-methods
|
||||
|
@ -37,15 +43,7 @@ diff --unified --recursive '--exclude=.pylint-disable.patch' original/api/api_ba
|
|||
diff --unified --recursive '--exclude=.pylint-disable.patch' original/api/api_tag.py patched/api/api_tag.py
|
||||
--- original/api/api_tag.py
|
||||
+++ patched/api/api_tag.py
|
||||
@@ -2,14 +2,14 @@
|
||||
api_tag.py : Trovebox Tag API Classes
|
||||
"""
|
||||
try:
|
||||
- from urllib.parse import quote # Python3
|
||||
+ from urllib.parse import quote # Python3 # pylint: disable=import-error,no-name-in-module
|
||||
except ImportError:
|
||||
from urllib import quote # Python2
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from trovebox.objects.tag import Tag
|
||||
from .api_base import ApiBase
|
||||
|
||||
|
@ -54,21 +52,18 @@ diff --unified --recursive '--exclude=.pylint-disable.patch' original/api/api_ta
|
|||
""" Definitions of /tags/ API endpoints """
|
||||
def list(self, **kwds):
|
||||
"""
|
||||
Only in patched/api: api_tag.py.~5~
|
||||
diff --unified --recursive '--exclude=.pylint-disable.patch' original/auth.py patched/auth.py
|
||||
--- original/auth.py
|
||||
+++ patched/auth.py
|
||||
@@ -4,7 +4,7 @@
|
||||
from __future__ import unicode_literals
|
||||
@@ -5,13 +5,13 @@
|
||||
import os
|
||||
import io
|
||||
try:
|
||||
- from configparser import ConfigParser # Python3
|
||||
+ from configparser import ConfigParser # Python3 # pylint: disable=import-error
|
||||
except ImportError:
|
||||
from ConfigParser import SafeConfigParser as ConfigParser # Python2
|
||||
try:
|
||||
@@ -12,9 +12,9 @@
|
||||
except ImportError: # pragma: no cover
|
||||
import StringIO as io # Python2
|
||||
|
||||
-class Auth(object):
|
||||
+class Auth(object): # pylint: disable=too-few-public-methods
|
||||
|
@ -78,7 +73,7 @@ diff --unified --recursive '--exclude=.pylint-disable.patch' original/auth.py pa
|
|||
consumer_key, consumer_secret,
|
||||
token, token_secret):
|
||||
if host is None:
|
||||
@@ -69,7 +69,7 @@
|
||||
@@ -66,7 +66,7 @@
|
||||
parser = ConfigParser()
|
||||
parser.optionxform = str # Case-sensitive options
|
||||
try:
|
||||
|
@ -170,7 +165,7 @@ diff --unified --recursive '--exclude=.pylint-disable.patch' original/main.py pa
|
|||
diff --unified --recursive '--exclude=.pylint-disable.patch' original/objects/trovebox_object.py patched/objects/trovebox_object.py
|
||||
--- original/objects/trovebox_object.py
|
||||
+++ patched/objects/trovebox_object.py
|
||||
@@ -5,7 +5,7 @@
|
||||
@@ -9,7 +9,7 @@
|
||||
""" Base object supporting the storage of custom fields as attributes """
|
||||
_type = "None"
|
||||
def __init__(self, client, json_dict):
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
"""Current version string"""
|
||||
__version__ = "0.6.1"
|
||||
__version__ = "0.6.2"
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
"""
|
||||
api_base.py: Base class for all API classes
|
||||
"""
|
||||
try:
|
||||
from urllib.parse import quote # Python3
|
||||
except ImportError:
|
||||
from urllib import quote # Python2
|
||||
|
||||
|
||||
class ApiBase(object):
|
||||
""" Base class for all API objects """
|
||||
def __init__(self, client):
|
||||
self._client = client
|
||||
|
||||
@staticmethod
|
||||
def _build_option_string(options):
|
||||
def _build_option_string(self, options):
|
||||
"""
|
||||
:param options: dictionary containing the options
|
||||
:returns: option_string formatted for an API endpoint
|
||||
|
@ -17,7 +21,7 @@ class ApiBase(object):
|
|||
if options is not None:
|
||||
for key in options:
|
||||
option_string += "/%s-%s" % (key, options[key])
|
||||
return option_string
|
||||
return self._quote_url(option_string)
|
||||
|
||||
@staticmethod
|
||||
def _extract_id(obj):
|
||||
|
@ -27,6 +31,11 @@ class ApiBase(object):
|
|||
except AttributeError:
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def _quote_url(string):
|
||||
""" Make a string suitable for insertion into a URL """
|
||||
return quote(string.encode('utf-8'))
|
||||
|
||||
@staticmethod
|
||||
def _result_to_list(result):
|
||||
""" Handle the case where the result contains no items """
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
"""
|
||||
api_tag.py : Trovebox Tag API Classes
|
||||
"""
|
||||
try:
|
||||
from urllib.parse import quote # Python3
|
||||
except ImportError:
|
||||
from urllib import quote # Python2
|
||||
|
||||
from trovebox.objects.tag import Tag
|
||||
from .api_base import ApiBase
|
||||
|
||||
|
@ -42,7 +37,7 @@ class ApiTag(ApiBase):
|
|||
Raises a TroveboxError if not.
|
||||
"""
|
||||
return self._client.post("/tag/%s/delete.json" %
|
||||
quote(self._extract_id(tag)),
|
||||
self._quote_url(self._extract_id(tag)),
|
||||
**kwds)["result"]
|
||||
|
||||
def update(self, tag, **kwds):
|
||||
|
@ -53,7 +48,7 @@ class ApiTag(ApiBase):
|
|||
Returns the updated tag object.
|
||||
"""
|
||||
result = self._client.post("/tag/%s/update.json" %
|
||||
quote(self._extract_id(tag)),
|
||||
self._quote_url(self._extract_id(tag)),
|
||||
**kwds)["result"]
|
||||
return Tag(self._client, result)
|
||||
|
||||
|
|
|
@ -193,37 +193,45 @@ class Http(object):
|
|||
endpoint = "/v%d%s" % (self.config["api_version"], endpoint)
|
||||
return urlunparse((scheme, host, endpoint, '', '', ''))
|
||||
|
||||
@staticmethod
|
||||
def _process_params(params):
|
||||
def _process_params(self, params):
|
||||
""" Converts Unicode/lists/booleans inside HTTP parameters """
|
||||
processed_params = {}
|
||||
for key, value in params.items():
|
||||
# Extract IDs from objects
|
||||
if isinstance(value, TroveboxObject):
|
||||
value = value.id
|
||||
|
||||
# Ensure value is UTF-8 encoded
|
||||
if isinstance(value, TEXT_TYPE):
|
||||
value = value.encode("utf-8")
|
||||
|
||||
# Handle lists
|
||||
if isinstance(value, list):
|
||||
# Make a copy of the list, to avoid overwriting the original
|
||||
new_list = list(value)
|
||||
# Extract IDs from objects in the list
|
||||
for i, item in enumerate(new_list):
|
||||
if isinstance(item, TroveboxObject):
|
||||
new_list[i] = item.id
|
||||
# Convert list to string
|
||||
value = ','.join([str(item) for item in new_list])
|
||||
|
||||
# Handle booleans
|
||||
if isinstance(value, bool):
|
||||
value = 1 if value else 0
|
||||
processed_params[key] = value
|
||||
processed_params[key] = self._process_param_value(value)
|
||||
|
||||
return processed_params
|
||||
|
||||
def _process_param_value(self, value):
|
||||
"""
|
||||
Returns a UTF-8 string representation of the parameter value,
|
||||
recursing into lists.
|
||||
"""
|
||||
# Extract IDs from objects
|
||||
if isinstance(value, TroveboxObject):
|
||||
return str(value.id).encode('utf-8')
|
||||
|
||||
# Ensure strings are UTF-8 encoded
|
||||
elif isinstance(value, TEXT_TYPE):
|
||||
return value.encode("utf-8")
|
||||
|
||||
# Handle lists
|
||||
elif isinstance(value, list):
|
||||
# Make a copy of the list, to avoid overwriting the original
|
||||
new_list = list(value)
|
||||
# Process each item in the list
|
||||
for i, item in enumerate(new_list):
|
||||
new_list[i] = self._process_param_value(item)
|
||||
# new_list elements are UTF-8 encoded strings - simply join up
|
||||
return b','.join(new_list)
|
||||
|
||||
# Handle booleans
|
||||
elif isinstance(value, bool):
|
||||
return b"1" if value else b"0"
|
||||
|
||||
# Unknown - just do our best
|
||||
else:
|
||||
return str(value).encode("utf-8")
|
||||
|
||||
@staticmethod
|
||||
def _process_response(response):
|
||||
"""
|
||||
|
|
|
@ -123,7 +123,14 @@ def extract_files(params):
|
|||
updated_params = {}
|
||||
for name in params:
|
||||
if name == "photo" and params[name].startswith("@"):
|
||||
files[name] = open(os.path.expanduser(params[name][1:]), 'rb')
|
||||
filename = params[name][1:]
|
||||
|
||||
# Python2 uses encoded commandline parameters.
|
||||
# Decode to Unicode if necessary.
|
||||
if isinstance(filename, bytes):
|
||||
filename = filename.decode(sys.getfilesystemencoding())
|
||||
|
||||
files[name] = open(os.path.expanduser(filename), 'rb')
|
||||
else:
|
||||
updated_params[name] = params[name]
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
"""
|
||||
Base object supporting the storage of custom fields as attributes
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import sys
|
||||
|
||||
class TroveboxObject(object):
|
||||
""" Base object supporting the storage of custom fields as attributes """
|
||||
_type = "None"
|
||||
|
@ -41,11 +45,17 @@ class TroveboxObject(object):
|
|||
|
||||
def __repr__(self):
|
||||
if self.name is not None:
|
||||
return "<%s name='%s'>" % (self.__class__.__name__, self.name)
|
||||
value = "<%s name='%s'>" % (self.__class__.__name__, self.name)
|
||||
elif self.id is not None:
|
||||
return "<%s id='%s'>" % (self.__class__.__name__, self.id)
|
||||
value = "<%s id='%s'>" % (self.__class__.__name__, self.id)
|
||||
else:
|
||||
return "<%s>" % (self.__class__.__name__)
|
||||
value = "<%s>" % (self.__class__.__name__)
|
||||
|
||||
# Python2 requires a bytestring
|
||||
if sys.version < '3':
|
||||
return value.encode('utf-8')
|
||||
else: # pragma: no cover
|
||||
return value
|
||||
|
||||
def get_fields(self):
|
||||
""" Returns this object's attributes """
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue