Enable deserialization of different content types coming from the server.
This commit is contained in:
@@ -16,6 +16,9 @@ Changelog
|
|||||||
* changed `perform_request` on `Connection` classes to return headers as well.
|
* changed `perform_request` on `Connection` classes to return headers as well.
|
||||||
This is a backwards incompatible change for people who have developed their own
|
This is a backwards incompatible change for people who have developed their own
|
||||||
connection class.
|
connection class.
|
||||||
|
* changed deserialization mechanics. Users who provided their own serializer
|
||||||
|
that didn't extend `JSONSerializer` need to specify a `mimetype` class
|
||||||
|
attribute.
|
||||||
|
|
||||||
0.4.3
|
0.4.3
|
||||||
-----
|
-----
|
||||||
|
|||||||
@@ -2,9 +2,23 @@ import json
|
|||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from .exceptions import SerializationError
|
from .exceptions import SerializationError, ImproperlyConfigured
|
||||||
|
|
||||||
|
class TextSerializer(object):
|
||||||
|
mimetype = 'text/plain'
|
||||||
|
|
||||||
|
def loads(self, s):
|
||||||
|
return s
|
||||||
|
|
||||||
|
def dumps(self, data):
|
||||||
|
if isinstance(data, (type(''), type(u''))):
|
||||||
|
return data
|
||||||
|
|
||||||
|
raise SerializationError('Cannot serialize %r into text.' % data)
|
||||||
|
|
||||||
class JSONSerializer(object):
|
class JSONSerializer(object):
|
||||||
|
mimetype = 'application/json'
|
||||||
|
|
||||||
def default(self, data):
|
def default(self, data):
|
||||||
if isinstance(data, (date, datetime)):
|
if isinstance(data, (date, datetime)):
|
||||||
return data.isoformat()
|
return data.isoformat()
|
||||||
@@ -28,4 +42,29 @@ class JSONSerializer(object):
|
|||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) as e:
|
||||||
raise SerializationError(data, e)
|
raise SerializationError(data, e)
|
||||||
|
|
||||||
|
DEFAULT_SERIALIZERS = {
|
||||||
|
JSONSerializer.mimetype: JSONSerializer(),
|
||||||
|
TextSerializer.mimetype: TextSerializer(),
|
||||||
|
}
|
||||||
|
|
||||||
|
class Deserializer(object):
|
||||||
|
def __init__(self, serializers, default_mimetype='application/json'):
|
||||||
|
try:
|
||||||
|
self.default = serializers[default_mimetype]
|
||||||
|
except KeyError:
|
||||||
|
raise ImproperlyConfigured('Cannot find default serializer (%s)' % default_mimetype)
|
||||||
|
self.serializers = serializers
|
||||||
|
|
||||||
|
def loads(self, s, mimetype=None):
|
||||||
|
if not mimetype:
|
||||||
|
deserializer = self.default
|
||||||
|
else:
|
||||||
|
# split out charset
|
||||||
|
mimetype = mimetype.split(';', 1)[0]
|
||||||
|
try:
|
||||||
|
deserializer = self.serializers[mimetype]
|
||||||
|
except KeyError:
|
||||||
|
raise SerializationError('Unknown mimetype, unable to deserialize: %s' % mimetype)
|
||||||
|
|
||||||
|
return deserializer.loads(s)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import time
|
|||||||
|
|
||||||
from .connection import Urllib3HttpConnection
|
from .connection import Urllib3HttpConnection
|
||||||
from .connection_pool import ConnectionPool
|
from .connection_pool import ConnectionPool
|
||||||
from .serializer import JSONSerializer
|
from .serializer import JSONSerializer, Deserializer, DEFAULT_SERIALIZERS
|
||||||
from .exceptions import ConnectionError, TransportError, SerializationError
|
from .exceptions import ConnectionError, TransportError, SerializationError
|
||||||
|
|
||||||
# get ip/port from "inet[wind/127.0.0.1:9200]"
|
# get ip/port from "inet[wind/127.0.0.1:9200]"
|
||||||
@@ -35,8 +35,9 @@ class Transport(object):
|
|||||||
def __init__(self, hosts, connection_class=Urllib3HttpConnection,
|
def __init__(self, hosts, connection_class=Urllib3HttpConnection,
|
||||||
connection_pool_class=ConnectionPool, host_info_callback=get_host_info,
|
connection_pool_class=ConnectionPool, host_info_callback=get_host_info,
|
||||||
sniff_on_start=False, sniffer_timeout=None,
|
sniff_on_start=False, sniffer_timeout=None,
|
||||||
sniff_on_connection_fail=False, serializer=JSONSerializer(),
|
sniff_on_connection_fail=False, serializer=JSONSerializer(), serializers=None,
|
||||||
max_retries=3, send_get_body_as='GET', **kwargs):
|
default_mimetype='application/json', max_retries=3,
|
||||||
|
send_get_body_as='GET', **kwargs):
|
||||||
"""
|
"""
|
||||||
:arg hosts: list of dictionaries, each containing keyword arguments to
|
:arg hosts: list of dictionaries, each containing keyword arguments to
|
||||||
create a `connection_class` instance
|
create a `connection_class` instance
|
||||||
@@ -50,6 +51,10 @@ class Transport(object):
|
|||||||
:arg sniffer_timeout: number of seconds between automatic sniffs
|
:arg sniffer_timeout: number of seconds between automatic sniffs
|
||||||
:arg sniff_on_connection_fail: flag controlling if connection failure triggers a sniff
|
:arg sniff_on_connection_fail: flag controlling if connection failure triggers a sniff
|
||||||
:arg serializer: serializer instance
|
:arg serializer: serializer instance
|
||||||
|
:arg serializers: optional dict of serializer instances that will be
|
||||||
|
used for deserializing data coming from the server. (key is the mimetype)
|
||||||
|
:arg default_mimetype: when no mimetype is specified by the server
|
||||||
|
response assume this mimetype, defaults to `'application/json'`
|
||||||
:arg max_retries: maximum number of retries before an exception is propagated
|
:arg max_retries: maximum number of retries before an exception is propagated
|
||||||
:arg send_get_body_as: for GET requests with body this option allows
|
:arg send_get_body_as: for GET requests with body this option allows
|
||||||
you to specify an alternate way of execution for environments that
|
you to specify an alternate way of execution for environments that
|
||||||
@@ -62,6 +67,16 @@ class Transport(object):
|
|||||||
options provided as part of the hosts parameter.
|
options provided as part of the hosts parameter.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# serialization config
|
||||||
|
_serializers = DEFAULT_SERIALIZERS.copy()
|
||||||
|
# if a serializer has been specified, use it for deserialization as well
|
||||||
|
_serializers[serializer.mimetype] = serializer
|
||||||
|
# if custom serializers map has been supplied, override the defaults with it
|
||||||
|
if serializers:
|
||||||
|
_serializers.update(serializers)
|
||||||
|
# create a deserializer with our config
|
||||||
|
self.deserializer = Deserializer(_serializers, default_mimetype)
|
||||||
|
|
||||||
self.max_retries = max_retries
|
self.max_retries = max_retries
|
||||||
self.send_get_body_as = send_get_body_as
|
self.send_get_body_as = send_get_body_as
|
||||||
|
|
||||||
@@ -155,7 +170,7 @@ class Transport(object):
|
|||||||
try:
|
try:
|
||||||
# use small timeout for the sniffing request, should be a fast api call
|
# use small timeout for the sniffing request, should be a fast api call
|
||||||
_, headers, node_info = c.perform_request('GET', '/_cluster/nodes', timeout=.1)
|
_, headers, node_info = c.perform_request('GET', '/_cluster/nodes', timeout=.1)
|
||||||
node_info = self.serializer.loads(node_info)
|
node_info = self.deserializer.loads(node_info, headers.get('content-type'))
|
||||||
break
|
break
|
||||||
except (ConnectionError, SerializationError):
|
except (ConnectionError, SerializationError):
|
||||||
pass
|
pass
|
||||||
@@ -263,6 +278,6 @@ class Transport(object):
|
|||||||
self.connection_pool.mark_live(connection)
|
self.connection_pool.mark_live(connection)
|
||||||
data = None
|
data = None
|
||||||
if raw_data:
|
if raw_data:
|
||||||
data = self.serializer.loads(raw_data)
|
data = self.deserializer.loads(raw_data, headers.get('content-type'))
|
||||||
return status, data
|
return status, data
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import sys
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from elasticsearch.serializer import JSONSerializer
|
from elasticsearch.serializer import JSONSerializer, Deserializer, DEFAULT_SERIALIZERS, TextSerializer
|
||||||
from elasticsearch.exceptions import SerializationError
|
from elasticsearch.exceptions import SerializationError, ImproperlyConfigured
|
||||||
|
|
||||||
from .test_cases import TestCase, SkipTest
|
from .test_cases import TestCase, SkipTest
|
||||||
|
|
||||||
@@ -28,3 +28,30 @@ class TestJSONSerializer(TestCase):
|
|||||||
|
|
||||||
def test_strings_are_left_untouched(self):
|
def test_strings_are_left_untouched(self):
|
||||||
self.assertEquals(u"你好", JSONSerializer().dumps(u"你好"))
|
self.assertEquals(u"你好", JSONSerializer().dumps(u"你好"))
|
||||||
|
|
||||||
|
|
||||||
|
class TestTextSerializer(TestCase):
|
||||||
|
def test_strings_are_left_untouched(self):
|
||||||
|
self.assertEquals(u"你好", TextSerializer().dumps(u"你好"))
|
||||||
|
|
||||||
|
def test_raises_serialization_error_on_dump_error(self):
|
||||||
|
self.assertRaises(SerializationError, TextSerializer().dumps, {})
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeserializer(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestDeserializer, self).setUp()
|
||||||
|
self.de = Deserializer(DEFAULT_SERIALIZERS)
|
||||||
|
|
||||||
|
def test_deserializes_json_by_default(self):
|
||||||
|
self.assertEquals({"some": "data"}, self.de.loads('{"some":"data"}'))
|
||||||
|
|
||||||
|
def test_deserializes_text_with_correct_ct(self):
|
||||||
|
self.assertEquals('{"some":"data"}', self.de.loads('{"some":"data"}', 'text/plain'))
|
||||||
|
self.assertEquals('{"some":"data"}', self.de.loads('{"some":"data"}', 'text/plain; charset=whatever'))
|
||||||
|
|
||||||
|
def test_raises_serialization_error_on_unknown_mimetype(self):
|
||||||
|
self.assertRaises(SerializationError, self.de.loads, '{}', 'text/html')
|
||||||
|
|
||||||
|
def test_raises_improperly_configured_when_default_mimetype_cannot_be_deserialized(self):
|
||||||
|
self.assertRaises(ImproperlyConfigured, Deserializer, {})
|
||||||
|
|||||||
Reference in New Issue
Block a user