summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.mailmap2
-rw-r--r--AUTHORS17
-rw-r--r--ChangeLog49
-rw-r--r--doc/source/cli.rst43
-rw-r--r--releasenotes/notes/310-notes-03040158a8683dd8.yaml38
-rw-r--r--swiftclient/client.py15
-rw-r--r--swiftclient/utils.py5
-rw-r--r--tests/functional/test_swiftclient.py11
-rw-r--r--tests/unit/test_swiftclient.py28
-rw-r--r--tests/unit/test_utils.py37
10 files changed, 234 insertions, 11 deletions
diff --git a/.mailmap b/.mailmap
index 840a8ac..859a978 100644
--- a/.mailmap
+++ b/.mailmap
@@ -89,3 +89,5 @@ Doug Hellmann <doug@doughellmann.com> <doug.hellmann@dreamhost.com>
Ondrej Novy <ondrej.novy@firma.seznam.cz>
James Nzomo <james@tdt.rocks> <kazikubwa@gmail.com>
Alessandro Pilotti <ap@pilotti.it> <apilotti@cloudbasesolutions.com>
+Marek Kaleta <marek.kaleta@firma.seznam.cz> <Marek.Kaleta@firma.seznam.cz>
+Andreas Jaeger <aj@suse.de> <aj@suse.com>
diff --git a/AUTHORS b/AUTHORS
index e7215de..f8b79c3 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,9 +1,12 @@
+Paul Belanger (pabelanger@redhat.com)
Christian Berendt (berendt@b1-systems.de)
Luis de Bethencourt (luis@debethencourt.com)
+Hu Bing (hubingsh@cn.ibm.com)
Darrell Bishop (darrell@swiftstack.com)
Fabien Boucher (fabien.boucher@enovance.com)
Chmouel Boudjnah (chmouel@enovance.com)
Clark Boylan (clark.boylan@gmail.com)
+Cedric Brandily (zzelle@gmail.com)
Chris Buccella (chris.buccella@antallagon.com)
Tim Burke (tim.burke@gmail.com)
Clint Byrum (clint@fewbar.com)
@@ -12,13 +15,13 @@ Sergio Cazzolato (sergio.j.cazzolato@intel.com)
Mahati Chamarthy (mahati.chamarthy@gmail.com)
Chaozhe.Chen (chaozhe.chen@easystack.cn)
Ray Chen (oldsharp@163.com)
-Li Cheng (shcli@cn.ibm.com)
Taurus Cheung (Taurus.Cheung@harmonicinc.com)
Alistair Coles (alistair.coles@hpe.com)
Ian Cordasco (ian.cordasco@rackspace.com)
Nick Craig-Wood (nick@craig-wood.com)
Thiago da Silva (thiago@redhat.com)
Sean Dague (sean@dague.net)
+Julien Danjou (julien@danjou.info)
Zack M. Davis (zdavis@swiftstack.com)
John Dickinson (me@not.mn)
EdLeafe (ed@leafe.com)
@@ -32,7 +35,9 @@ Anne Gentle (anne@openstack.org)
Clay Gerrard (clay.gerrard@gmail.com)
David Goetz (david.goetz@rackspace.com)
Thomas Goirand (thomas@goirand.fr)
+Sergey Gotliv (sgotliv@redhat.com)
Davide Guerri (davide.guerri@hp.com)
+Shashirekha Gundur (shashirekha.j.gundur@intel.com)
Romain Hardouin (romain_hardouin@yahoo.fr)
Steven Hardy (shardy@redhat.com)
Doug Hellmann (doug@doughellmann.com)
@@ -44,12 +49,15 @@ Andreas Jaeger (aj@suse.de)
Jude Job (judeopenstack@gmail.com)
Vasyl Khomenko (vasiliyk@yahoo-inc.com)
Leah Klearman (lklrmn@gmail.com)
+Marek Kaleta (marek.kaleta@firma.seznam.cz)
Jaivish Kothari (jaivish.kothari@nectechnologies.in)
Jakub Krajcovic (jakub.krajcovic@gmail.com)
David Kranz (david.kranz@qrclab.com)
Sushil Kumar (sushil.kumar2@globallogic.com)
Greg Lange (greglange@gmail.com)
Alexis Lee (lxsli@hpe.com)
+Jamie Lennox (jamielennox@gmail.com)
+Cheng Li (shcli@cn.ibm.com)
Tong Li (litong01@us.ibm.com)
Peter Lisak (peter.lisak@firma.seznam.cz)
Feng Liu (mefengliu23@gmail.com)
@@ -71,6 +79,7 @@ Dirk Mueller (dirk@dmllr.de)
Zhenguo Niu (zhenguo@unitedstack.com)
Ondrej Novy (ondrej.novy@firma.seznam.cz)
James Nzomo (james@tdt.rocks)
+Nguyen Hung Phuong (phuongnh@vn.fujitsu.com)
Alessandro Pilotti (ap@pilotti.it)
Stanislaw Pitucha (stanislaw.pitucha@hpe.com)
Dan Prince (dprince@redhat.com)
@@ -87,18 +96,22 @@ Jeremy Stanley (fungi@yuggoth.org)
Victor Stinner (victor.stinner@enovance.com)
Jiří Suchomel (jsuchome@suse.cz)
YUZAWA Takahiko (yuzawataka@intellilink.co.jp)
+Nandini Tata (nandini.tata.15@gmail.com)
Monty Taylor (mordred@inaugust.com)
TheSriram (sriram@klusterkloud.com)
Tihomir Trifonov (t.trifonov@gmail.com)
Dean Troyer (dtroyer@gmail.com)
+Kota Tsuyuzaki (tsuyuzaki.kota@lab.ntt.co.jp)
Stanislav Vitkovskiy (stas.vitkovsky@gmail.com)
Daniel Wakefield (daniel.wakefield@hp.com)
Shane Wang (shane.wang@intel.com)
Mark Washenberger (mark.washenberger@rackspace.com)
+Andrew Welleck (awellec@us.ibm.com)
Wu Wenxiang (wu.wenxiang@99cloud.net)
Mike Widman (mwidman@endurancewindpower.com)
Joel Wright (joel.wright@sohonet.com)
You Yamagata (bi.yamagata@gmail.com)
+zheng yin (yin.zheng@easystack.cn)
Qiu Yu (qiuyu@ebaysf.com)
YangLei (yanglyy@cn.ibm.com)
Pete Zaitcev (zaitcev@kotori.zaitcev.us)
@@ -112,3 +125,5 @@ zhang-jinnan (ben.os@99cloud.net)
hgangwx (hgangwx@cn.ibm.com)
shu-mutou (shu-mutou@rf.jp.nec.com)
SaiKiran (saikiranveeravarapu@gmail.com)
+venkatamahesh (venkatamaheshkotha@gmail.com)
+yuyafei (yu.yafei@zte.com.cn)
diff --git a/ChangeLog b/ChangeLog
index f5e0a60..7697bdb 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,52 @@
+3.1.0
+-----
+
+* Added a copy object method.
+
+* Arbitrary query strings can now be passed into container functions.
+
+* Client certificate and key can now be specified via CLI
+ options (--os-cert/--os-key) or environment variables ($OS_CERT/$OS_KEY).
+
+* A new CLI option `--ignore-checksum` can be specified to turn off
+ checksum validation. In the SDK, the new `checksum=True` parameter can
+ be used for the same purpose.
+
+* Added --json option to `swift capabilities` / `swift info`
+
+* Default to v3 auth if we find a (user|project)-domain-(name|id) option.
+
+* Added a Python version constraint of >= Py27.
+
+* `client.py` will now retry on a 401 (auth error) even if `retries` is
+ set to zero.
+
+* Fixed `swift download` when `marker` was specified.
+
+* Object segments uploaded via swiftclient are now given the content type
+ "application/swiftclient-segment".
+
+* "Directory marker" objects are now given a "application/directory"
+ content type to match both Swift's `staticweb` feature and other
+ ecosystem tools.
+
+* Strip leading/trailing whitespace from headers (otherwise, new versions
+ of the requests library will raise an InvalidHeader error). Additionally,
+ header values with standard types (integer, float, or bool) are coerced
+ to strings before being sent to a socket.
+
+* Non-python dependencies are now specified in bindep.txt. Currently this
+ only lists a single dependency for testing (PyPy), but if future
+ dependencies are added, they will be included in this file.
+
+* Client exceptions now include response headers. One benefit is that
+ this allows clients to see transaction IDs without needing to turn on
+ debug logging.
+
+* Client connections now accept gzip-encoded responses.
+
+* Various other minor bug fixes and improvements.
+
3.0.0
-----
diff --git a/doc/source/cli.rst b/doc/source/cli.rst
index 9bb229d..87020c9 100644
--- a/doc/source/cli.rst
+++ b/doc/source/cli.rst
@@ -225,6 +225,28 @@ Capabilities
the ``proxy-url`` option is not provided, the storage URL retrieved after
authentication is used as ``proxy-url``.
+Tempurl
+-------
+
+ ``tempurl [method] [seconds] [path] [key]``
+
+ Generates a temporary URL for a Swift object. ``method`` option sets an HTTP method to
+ allow for this temporary URL that is usually 'GET' or 'PUT'. ``seconds`` option sets
+ the amount of time in seconds the temporary URL will be valid for; or, if ``--absolute``
+ is passed, the Unix timestamp when the temporary URL will expire. ``path`` option sets
+ the full path to the Swift object. Example: ``/v1/AUTH_account/c/o``. ``key`` option is
+ the secret temporary URL key set on the Swift cluster. To set a key, run
+ ``swift post -m "Temp-URL-Key: <your secret key>"``.
+
+Auth
+----
+
+ ``auth``
+
+ Display authentication variables in shell friendly format. Command to run to export storage
+ url and auth token into ``OS_STORAGE_URL`` and ``OS_AUTH_TOKEN``: ``swift auth``.
+ Command to append to a runcom file (e.g. ``~/.bashrc``, ``/etc/profile``) for automatic
+ authentication: ``swift auth -v -U test:tester -K testing``.
Examples
~~~~~~~~
@@ -272,6 +294,15 @@ List the contents of a container:
testSwift.txt
+Display auth related authentication variables in shell friendly format:
+
+.. code-block:: bash
+
+ > swift auth
+
+ export OS_STORAGE_URL=http://127.0.0.1:8080/v1/AUTH_bf5e63572f7a420a83fcf0aa8c72c2c7
+ export OS_AUTH_TOKEN=c597015ae19943a18438b52ef3762e79
+
Download an object from a container:
.. code-block:: bash
@@ -347,3 +378,15 @@ For more information on large objects, see the documentation `here
myvideo.mp4/slo/1460229233.679546/9341553868/1073741824/00000006
myvideo.mp4/slo/1460229233.679546/9341553868/1073741824/00000007
myvideo.mp4/slo/1460229233.679546/9341553868/1073741824/00000008
+
+Firstly, the key should be set, then generate a temporary URL for a Swift object:
+
+.. code-block:: bash
+
+ > swift post -m "Temp-URL-Key:b3968d0207b54ece87cccc06515a89d4"
+
+ > swift tempurl GET 6000 /v1/AUTH_bf5e63572f7a420a83fcf0aa8c72c2c7\
+ /firstcontainer/clean.sh b3968d0207b54ece87cccc06515a89d4
+
+ /v1/AUTH_/firstcontainer/clean.sh?temp_url_sig=\
+ 9218fc288cc09e5edd857b6a3d43cf2122b906dc&temp_url_expires=1472203614
diff --git a/releasenotes/notes/310-notes-03040158a8683dd8.yaml b/releasenotes/notes/310-notes-03040158a8683dd8.yaml
new file mode 100644
index 0000000..792b6f5
--- /dev/null
+++ b/releasenotes/notes/310-notes-03040158a8683dd8.yaml
@@ -0,0 +1,38 @@
+---
+features:
+ - Added a copy object method.
+ - Arbitrary query strings can now be passed into container functions.
+ - >
+ Client certificate and key can now be specified via CLI
+ options (--os-cert/--os-key) or environment variables ($OS_CERT/$OS_KEY).
+ - >
+ A new CLI option `--ignore-checksum` can be specified to turn off
+ checksum validation. In the SDK, the new `checksum=True` parameter can
+ be used for the same purpose.
+ - Added --json option to `swift capabilities` / `swift info`
+ - Default to v3 auth if we find a (user|project)-domain-(name|id) option.
+ - Added a Python version constraint of >= Py27.
+ - >
+ `client.py` will now retry on a 401 (auth error) even if `retries` is
+ set to zero.
+ - Fixed `swift download` when `marker` was specified.
+ - Object segments uploaded via swiftclient are now given the content type
+ "application/swiftclient-segment".
+ - >
+ "Directory marker" objects are now given a "application/directory"
+ content type to match both Swift's `staticweb` feature and other
+ ecosystem tools.
+ - >
+ Strip leading/trailing whitespace from headers (otherwise, new versions
+ of the requests library will raise an InvalidHeader error). Additionally,
+ header values with standard types (integer, float, or bool) are coerced
+ to strings before being sent to a socket.
+ - >
+ Non-python dependencies are now specified in bindep.txt. Currently this
+ only lists a single dependency for testing (PyPy), but if future
+ dependencies are added, they will be included in this file.
+ - Client exceptions now include response headers. One benefit is that
+ this allows clients to see transaction IDs without needing to turn on
+ debug logging.
+ - Client connections now accept gzip-encoded responses.
+ - Various other minor bug fixes and improvements.
diff --git a/swiftclient/client.py b/swiftclient/client.py
index 988c7d9..ee5a838 100644
--- a/swiftclient/client.py
+++ b/swiftclient/client.py
@@ -211,6 +211,13 @@ def quote(value, safe='/'):
def encode_utf8(value):
+ if type(value) in six.integer_types + (float, bool):
+ # As of requests 2.11.0, headers must be byte- or unicode-strings.
+ # Convert some known-good types as a convenience for developers.
+ # Note that we *don't* convert subclasses, as they may have overriddden
+ # __str__ or __repr__.
+ # See https://github.com/kennethreitz/requests/pull/3366 for more info
+ value = str(value)
if isinstance(value, six.text_type):
value = value.encode('utf8')
return value
@@ -732,7 +739,7 @@ def get_account(url, token, marker=None, limit=None, prefix=None,
if end_marker:
qs += '&end_marker=%s' % quote(end_marker)
full_path = '%s?%s' % (parsed.path, qs)
- headers = {'X-Auth-Token': token}
+ headers = {'X-Auth-Token': token, 'Accept-Encoding': 'gzip'}
if service_token:
headers['X-Service-Token'] = service_token
method = 'GET'
@@ -859,6 +866,7 @@ def get_container(url, token, container, marker=None, limit=None,
else:
headers = {}
headers['X-Auth-Token'] = token
+ headers['Accept-Encoding'] = 'gzip'
if full_listing:
rv = get_container(url, token, container, marker, limit, prefix,
delimiter, end_marker, path, http_conn,
@@ -1457,10 +1465,11 @@ def get_capabilities(http_conn):
:raises ClientException: HTTP Capabilities GET failed
"""
parsed, conn = http_conn
- conn.request('GET', parsed.path, '')
+ headers = {'Accept-Encoding': 'gzip'}
+ conn.request('GET', parsed.path, '', headers)
resp = conn.getresponse()
body = resp.read()
- http_log((parsed.geturl(), 'GET',), {'headers': {}}, resp, body)
+ http_log((parsed.geturl(), 'GET',), {'headers': headers}, resp, body)
if resp.status < 200 or resp.status >= 300:
raise ClientException.from_response(
resp, 'Capabilities GET failed', body)
diff --git a/swiftclient/utils.py b/swiftclient/utils.py
index 10687bf..d394283 100644
--- a/swiftclient/utils.py
+++ b/swiftclient/utils.py
@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Miscellaneous utility functions for use with Swift."""
+import gzip
import hashlib
import hmac
import json
@@ -120,6 +121,10 @@ def generate_temp_url(path, seconds, key, method, absolute=False):
def parse_api_response(headers, body):
+ if headers.get('content-encoding') == 'gzip':
+ with gzip.GzipFile(fileobj=six.BytesIO(body), mode='r') as gz:
+ body = gz.read()
+
charset = 'utf-8'
# Swift *should* be speaking UTF-8, but check content-type just in case
content_type = headers.get('content-type', '')
diff --git a/tests/functional/test_swiftclient.py b/tests/functional/test_swiftclient.py
index 6e19abd..0e6a346 100644
--- a/tests/functional/test_swiftclient.py
+++ b/tests/functional/test_swiftclient.py
@@ -400,10 +400,19 @@ class TestFunctional(unittest.TestCase):
def test_post_object(self):
self.conn.post_object(self.containername,
self.objectname,
- {'x-object-meta-color': 'Something'})
+ {'x-object-meta-color': 'Something',
+ 'x-object-meta-uni': b'\xd8\xaa'.decode('utf8'),
+ 'x-object-meta-int': 123,
+ 'x-object-meta-float': 45.67,
+ 'x-object-meta-bool': False})
headers = self.conn.head_object(self.containername, self.objectname)
self.assertEqual('Something', headers.get('x-object-meta-color'))
+ self.assertEqual(b'\xd8\xaa'.decode('utf-8'),
+ headers.get('x-object-meta-uni'))
+ self.assertEqual('123', headers.get('x-object-meta-int'))
+ self.assertEqual('45.67', headers.get('x-object-meta-float'))
+ self.assertEqual('False', headers.get('x-object-meta-bool'))
def test_copy_object(self):
self.conn.put_object(
diff --git a/tests/unit/test_swiftclient.py b/tests/unit/test_swiftclient.py
index 0095447..7d0dd9a 100644
--- a/tests/unit/test_swiftclient.py
+++ b/tests/unit/test_swiftclient.py
@@ -185,9 +185,9 @@ class TestHttpHelpers(MockHttpTest):
def test_encode_meta_headers(self):
headers = {'abc': '123',
- u'x-container-meta-\u0394': '123',
- u'x-account-meta-\u0394': '123',
- u'x-object-meta-\u0394': '123'}
+ u'x-container-meta-\u0394': 123,
+ u'x-account-meta-\u0394': 12.3,
+ u'x-object-meta-\u0394': True}
r = swiftclient.encode_meta_headers(headers)
@@ -199,6 +199,7 @@ class TestHttpHelpers(MockHttpTest):
for k, v in r.items():
self.assertIs(type(k), binary_type)
self.assertIs(type(v), binary_type)
+ self.assertIn(v, (b'123', b'12.3', b'True'))
def test_set_user_agent_default(self):
_junk, conn = c.http_connection('http://www.example.com')
@@ -581,6 +582,7 @@ class TestGetAccount(MockHttpTest):
self.assertEqual(value, [])
self.assertRequests([
('GET', '/v1/acct?format=json', '', {
+ 'accept-encoding': 'gzip',
'x-auth-token': 'asdf'}),
])
@@ -591,6 +593,7 @@ class TestGetAccount(MockHttpTest):
c.get_account('http://www.test.com/v1/acct', 'asdf', marker='marker')
self.assertRequests([
('GET', '/v1/acct?format=json&marker=marker', '', {
+ 'accept-encoding': 'gzip',
'x-auth-token': 'asdf'}),
])
@@ -601,6 +604,7 @@ class TestGetAccount(MockHttpTest):
c.get_account('http://www.test.com/v1/acct', 'asdf', limit=10)
self.assertRequests([
('GET', '/v1/acct?format=json&limit=10', '', {
+ 'accept-encoding': 'gzip',
'x-auth-token': 'asdf'}),
])
@@ -611,6 +615,7 @@ class TestGetAccount(MockHttpTest):
c.get_account('http://www.test.com/v1/acct', 'asdf', prefix='asdf/')
self.assertRequests([
('GET', '/v1/acct?format=json&prefix=asdf/', '', {
+ 'accept-encoding': 'gzip',
'x-auth-token': 'asdf'}),
])
@@ -622,6 +627,7 @@ class TestGetAccount(MockHttpTest):
end_marker='end_marker')
self.assertRequests([
('GET', '/v1/acct?format=json&end_marker=end_marker', '', {
+ 'accept-encoding': 'gzip',
'x-auth-token': 'asdf'}),
])
@@ -700,6 +706,7 @@ class TestGetContainer(MockHttpTest):
self.assertEqual(value, [])
self.assertRequests([
('GET', '/v1/acct/container?format=json', '', {
+ 'accept-encoding': 'gzip',
'x-auth-token': 'token'}),
])
@@ -711,6 +718,7 @@ class TestGetContainer(MockHttpTest):
marker='marker')
self.assertRequests([
('GET', '/v1/acct/container?format=json&marker=marker', '', {
+ 'accept-encoding': 'gzip',
'x-auth-token': 'token'}),
])
@@ -722,6 +730,7 @@ class TestGetContainer(MockHttpTest):
limit=10)
self.assertRequests([
('GET', '/v1/acct/container?format=json&limit=10', '', {
+ 'accept-encoding': 'gzip',
'x-auth-token': 'token'}),
])
@@ -733,6 +742,7 @@ class TestGetContainer(MockHttpTest):
prefix='asdf/')
self.assertRequests([
('GET', '/v1/acct/container?format=json&prefix=asdf/', '', {
+ 'accept-encoding': 'gzip',
'x-auth-token': 'token'}),
])
@@ -744,6 +754,7 @@ class TestGetContainer(MockHttpTest):
delimiter='/')
self.assertRequests([
('GET', '/v1/acct/container?format=json&delimiter=/', '', {
+ 'accept-encoding': 'gzip',
'x-auth-token': 'token'}),
])
@@ -755,7 +766,7 @@ class TestGetContainer(MockHttpTest):
end_marker='end_marker')
self.assertRequests([
('GET', '/v1/acct/container?format=json&end_marker=end_marker',
- '', {'x-auth-token': 'token'}),
+ '', {'x-auth-token': 'token', 'accept-encoding': 'gzip'}),
])
def test_param_path(self):
@@ -766,6 +777,7 @@ class TestGetContainer(MockHttpTest):
path='asdf')
self.assertRequests([
('GET', '/v1/acct/container?format=json&path=asdf', '', {
+ 'accept-encoding': 'gzip',
'x-auth-token': 'token'}),
])
@@ -780,6 +792,7 @@ class TestGetContainer(MockHttpTest):
('GET', '/container?format=json', '', {
'x-auth-token': 'TOKEN',
'x-client-key': 'client key',
+ 'accept-encoding': 'gzip',
}),
])
@@ -790,6 +803,7 @@ class TestGetContainer(MockHttpTest):
query_string="hello=20")
self.assertRequests([
('GET', '/asdf?format=json&hello=20', '', {
+ 'accept-encoding': 'gzip',
'x-auth-token': 'asdf'}),
])
@@ -1583,7 +1597,7 @@ class TestGetCapabilities(MockHttpTest):
http_conn = conn('http://www.test.com/info')
info = c.get_capabilities(http_conn)
self.assertRequests([
- ('GET', '/info', '', {}),
+ ('GET', '/info', '', {'Accept-Encoding': 'gzip'}),
])
self.assertEqual(info, {})
self.assertTrue(http_conn[1].resp.has_been_read)
@@ -1619,7 +1633,8 @@ class TestGetCapabilities(MockHttpTest):
('GET', '/auth/v1.0', '', {
'x-auth-user': 'user',
'x-auth-key': 'key'}),
- ('GET', 'http://storage.example.com/info', '', {}),
+ ('GET', 'http://storage.example.com/info', '', {
+ 'accept-encoding': 'gzip'}),
])
def test_conn_get_capabilities_with_os_auth(self):
@@ -2341,6 +2356,7 @@ class TestConnection(MockHttpTest):
('GET', '/v1/a/c1?format=json&limit=5&prefix=p', '', {
'x-auth-token': 'token',
'X-Favourite-Pet': 'Aardvark',
+ 'accept-encoding': 'gzip',
}),
])
self.assertEqual(conn.attempts, 1)
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index 0f210a3..f0de79c 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import gzip
import unittest
import mock
import six
@@ -394,3 +395,39 @@ class TestGroupers(unittest.TestCase):
result = list(u.n_groups(range(100), 12))
self.assertEqual([9] * 11 + [1], list(map(len, result)))
+
+
+class TestApiResponeParser(unittest.TestCase):
+
+ def test_utf8_default(self):
+ result = u.parse_api_response(
+ {}, u'{"test": "\u2603"}'.encode('utf8'))
+ self.assertEqual({'test': u'\u2603'}, result)
+
+ result = u.parse_api_response(
+ {}, u'{"test": "\\u2603"}'.encode('utf8'))
+ self.assertEqual({'test': u'\u2603'}, result)
+
+ def test_bad_json(self):
+ self.assertRaises(ValueError, u.parse_api_response,
+ {}, b'{"foo": "bar}')
+
+ def test_bad_utf8(self):
+ self.assertRaises(UnicodeDecodeError, u.parse_api_response,
+ {}, b'{"foo": "b\xffr"}')
+
+ def test_latin_1(self):
+ result = u.parse_api_response(
+ {'content-type': 'application/json; charset=iso8859-1'},
+ b'{"t\xe9st": "\xff"}')
+ self.assertEqual({u't\xe9st': u'\xff'}, result)
+
+ def test_gzipped_utf8(self):
+ buf = six.BytesIO()
+ gz = gzip.GzipFile(fileobj=buf, mode='w')
+ gz.write(u'{"test": "\u2603"}'.encode('utf8'))
+ gz.close()
+ result = u.parse_api_response(
+ {'content-encoding': 'gzip'},
+ buf.getvalue())
+ self.assertEqual({'test': u'\u2603'}, result)