[7.x] Support serializing numpy and pandas types
This commit is contained in:
@@ -6,6 +6,8 @@ nosexcover
|
|||||||
sphinx<1.7
|
sphinx<1.7
|
||||||
sphinx_rtd_theme
|
sphinx_rtd_theme
|
||||||
jinja2
|
jinja2
|
||||||
|
numpy
|
||||||
|
pandas
|
||||||
|
|
||||||
# PyYAML 5.3 dropped support for Python 3.4 while
|
# PyYAML 5.3 dropped support for Python 3.4 while
|
||||||
# not amending that requirement to the package. :(
|
# not amending that requirement to the package. :(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ try:
|
|||||||
import simplejson as json
|
import simplejson as json
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
@@ -9,6 +10,41 @@ from decimal import Decimal
|
|||||||
from .exceptions import SerializationError, ImproperlyConfigured
|
from .exceptions import SerializationError, ImproperlyConfigured
|
||||||
from .compat import string_types
|
from .compat import string_types
|
||||||
|
|
||||||
|
INTEGER_TYPES = ()
|
||||||
|
FLOAT_TYPES = (Decimal,)
|
||||||
|
TIME_TYPES = (date, datetime)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
INTEGER_TYPES += (
|
||||||
|
np.int_,
|
||||||
|
np.intc,
|
||||||
|
np.int8,
|
||||||
|
np.int16,
|
||||||
|
np.int32,
|
||||||
|
np.int64,
|
||||||
|
np.uint8,
|
||||||
|
np.uint16,
|
||||||
|
np.uint32,
|
||||||
|
np.uint64,
|
||||||
|
)
|
||||||
|
FLOAT_TYPES += (
|
||||||
|
np.float_,
|
||||||
|
np.float16,
|
||||||
|
np.float32,
|
||||||
|
np.float64,
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
np = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
TIME_TYPES += (pd.Timestamp,)
|
||||||
|
except ImportError:
|
||||||
|
pd = None
|
||||||
|
|
||||||
|
|
||||||
class TextSerializer(object):
|
class TextSerializer(object):
|
||||||
mimetype = "text/plain"
|
mimetype = "text/plain"
|
||||||
@@ -27,12 +63,29 @@ class JSONSerializer(object):
|
|||||||
mimetype = "application/json"
|
mimetype = "application/json"
|
||||||
|
|
||||||
def default(self, data):
|
def default(self, data):
|
||||||
if isinstance(data, (date, datetime)):
|
if isinstance(data, TIME_TYPES):
|
||||||
return data.isoformat()
|
return data.isoformat()
|
||||||
elif isinstance(data, Decimal):
|
|
||||||
return float(data)
|
|
||||||
elif isinstance(data, uuid.UUID):
|
elif isinstance(data, uuid.UUID):
|
||||||
return str(data)
|
return str(data)
|
||||||
|
elif isinstance(data, FLOAT_TYPES):
|
||||||
|
return float(data)
|
||||||
|
elif INTEGER_TYPES and isinstance(data, INTEGER_TYPES):
|
||||||
|
return int(data)
|
||||||
|
|
||||||
|
# Special cases for numpy and pandas types
|
||||||
|
elif np:
|
||||||
|
if isinstance(data, np.bool_):
|
||||||
|
return bool(data)
|
||||||
|
elif isinstance(data, np.datetime64):
|
||||||
|
return data.item().isoformat()
|
||||||
|
elif isinstance(data, np.ndarray):
|
||||||
|
return data.tolist()
|
||||||
|
if pd:
|
||||||
|
if isinstance(data, (pd.Series, pd.Categorical)):
|
||||||
|
return data.tolist()
|
||||||
|
elif hasattr(pd, "NA") and pd.isna(data):
|
||||||
|
return None
|
||||||
|
|
||||||
raise TypeError("Unable to serialize %r (type: %s)" % (data, type(data)))
|
raise TypeError("Unable to serialize %r (type: %s)" % (data, type(data)))
|
||||||
|
|
||||||
def loads(self, s):
|
def loads(self, s):
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import uuid
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
from elasticsearch.serializer import (
|
from elasticsearch.serializer import (
|
||||||
JSONSerializer,
|
JSONSerializer,
|
||||||
Deserializer,
|
Deserializer,
|
||||||
@@ -36,6 +39,86 @@ class TestJSONSerializer(TestCase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_serializes_numpy_bool(self):
|
||||||
|
self.assertEquals('{"d":true}', JSONSerializer().dumps({"d": np.bool_(True)}))
|
||||||
|
|
||||||
|
def test_serializes_numpy_integers(self):
|
||||||
|
ser = JSONSerializer()
|
||||||
|
for np_type in (
|
||||||
|
np.int_,
|
||||||
|
np.int8,
|
||||||
|
np.int16,
|
||||||
|
np.int32,
|
||||||
|
np.int64,
|
||||||
|
):
|
||||||
|
self.assertEquals(ser.dumps({"d": np_type(-1)}), '{"d":-1}')
|
||||||
|
|
||||||
|
for np_type in (
|
||||||
|
np.uint8,
|
||||||
|
np.uint16,
|
||||||
|
np.uint32,
|
||||||
|
np.uint64,
|
||||||
|
):
|
||||||
|
self.assertEquals(ser.dumps({"d": np_type(1)}), '{"d":1}')
|
||||||
|
|
||||||
|
def test_serializes_numpy_floats(self):
|
||||||
|
ser = JSONSerializer()
|
||||||
|
for np_type in (
|
||||||
|
np.float_,
|
||||||
|
np.float32,
|
||||||
|
np.float64,
|
||||||
|
):
|
||||||
|
self.assertRegexpMatches(
|
||||||
|
ser.dumps({"d": np_type(1.2)}), r'^\{"d":1\.2[\d]*}$'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_serializes_numpy_datetime(self):
|
||||||
|
self.assertEquals(
|
||||||
|
'{"d":"2010-10-01T02:30:00"}',
|
||||||
|
JSONSerializer().dumps({"d": np.datetime64("2010-10-01T02:30:00")}),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_serializes_numpy_ndarray(self):
|
||||||
|
self.assertEquals(
|
||||||
|
'{"d":[0,0,0,0,0]}',
|
||||||
|
JSONSerializer().dumps({"d": np.zeros((5,), dtype=np.uint8)}),
|
||||||
|
)
|
||||||
|
# This isn't useful for Elasticsearch, just want to make sure it works.
|
||||||
|
self.assertEquals(
|
||||||
|
'{"d":[[0,0],[0,0]]}',
|
||||||
|
JSONSerializer().dumps({"d": np.zeros((2, 2), dtype=np.uint8)}),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_serializes_pandas_timestamp(self):
|
||||||
|
self.assertEquals(
|
||||||
|
'{"d":"2010-10-01T02:30:00"}',
|
||||||
|
JSONSerializer().dumps({"d": pd.Timestamp("2010-10-01T02:30:00")}),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_serializes_pandas_series(self):
|
||||||
|
self.assertEquals(
|
||||||
|
'{"d":["a","b","c","d"]}',
|
||||||
|
JSONSerializer().dumps({"d": pd.Series(["a", "b", "c", "d"])}),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_serializes_pandas_na(self):
|
||||||
|
if not hasattr(pd, "NA"): # pandas.NA added in v1
|
||||||
|
raise SkipTest("pandas.NA required")
|
||||||
|
self.assertEquals(
|
||||||
|
'{"d":null}', JSONSerializer().dumps({"d": pd.NA}),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_serializes_pandas_category(self):
|
||||||
|
cat = pd.Categorical(["a", "c", "b", "a"], categories=["a", "b", "c"])
|
||||||
|
self.assertEquals(
|
||||||
|
'{"d":["a","c","b","a"]}', JSONSerializer().dumps({"d": cat}),
|
||||||
|
)
|
||||||
|
|
||||||
|
cat = pd.Categorical([1, 2, 3], categories=[1, 2, 3])
|
||||||
|
self.assertEquals(
|
||||||
|
'{"d":[1,2,3]}', JSONSerializer().dumps({"d": cat}),
|
||||||
|
)
|
||||||
|
|
||||||
def test_raises_serialization_error_on_dump_error(self):
|
def test_raises_serialization_error_on_dump_error(self):
|
||||||
self.assertRaises(SerializationError, JSONSerializer().dumps, object())
|
self.assertRaises(SerializationError, JSONSerializer().dumps, object())
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ SKIP_TESTS = {
|
|||||||
"TestIndicesGetAlias10Basic",
|
"TestIndicesGetAlias10Basic",
|
||||||
# Disallowing expensive queries is 7.7+
|
# Disallowing expensive queries is 7.7+
|
||||||
"TestSearch320DisallowQueries",
|
"TestSearch320DisallowQueries",
|
||||||
|
"TestIndicesPutIndexTemplate10Basic",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user