[7.x] Support serializing numpy and pandas types

This commit is contained in:
Seth Michael Larson
2020-03-30 13:02:18 -05:00
committed by GitHub
parent 98bee5c1e2
commit 606287f041
4 changed files with 142 additions and 3 deletions
+2
View File
@@ -6,6 +6,8 @@ nosexcover
sphinx<1.7
sphinx_rtd_theme
jinja2
numpy
pandas
# PyYAML 5.3 dropped support for Python 3.4 while
# not amending that requirement to the package. :(
+56 -3
View File
@@ -2,6 +2,7 @@ try:
import simplejson as json
except ImportError:
import json
import uuid
from datetime import date, datetime
from decimal import Decimal
@@ -9,6 +10,41 @@ from decimal import Decimal
from .exceptions import SerializationError, ImproperlyConfigured
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):
mimetype = "text/plain"
@@ -27,12 +63,29 @@ class JSONSerializer(object):
mimetype = "application/json"
def default(self, data):
if isinstance(data, (date, datetime)):
if isinstance(data, TIME_TYPES):
return data.isoformat()
elif isinstance(data, Decimal):
return float(data)
elif isinstance(data, uuid.UUID):
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)))
def loads(self, s):
+83
View File
@@ -5,6 +5,9 @@ import uuid
from datetime import datetime
from decimal import Decimal
import numpy as np
import pandas as pd
from elasticsearch.serializer import (
JSONSerializer,
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):
self.assertRaises(SerializationError, JSONSerializer().dumps, object())
@@ -39,6 +39,7 @@ SKIP_TESTS = {
"TestIndicesGetAlias10Basic",
# Disallowing expensive queries is 7.7+
"TestSearch320DisallowQueries",
"TestIndicesPutIndexTemplate10Basic",
}
}