import base64
import ipaddress
import collections
from datetime import date, datetime
from dateutil import parser, tz
from six import itervalues, string_types, iteritems
from six.moves import map
from .utils import DslBase, ObjectBase, AttrDict, AttrList
from .exceptions import ValidationException
unicode = type(u'')
def construct_field(name_or_field, **params):
# {"type": "text", "analyzer": "snowball"}
if isinstance(name_or_field, collections.Mapping):
if params:
raise ValueError('construct_field() cannot accept parameters when passing in a dict.')
params = name_or_field.copy()
if 'type' not in params:
# inner object can be implicitly defined
if 'properties' in params:
name = 'object'
else:
raise ValueError('construct_field() needs to have a "type" key.')
else:
name = params.pop('type')
return Field.get_dsl_class(name)(**params)
# Text()
if isinstance(name_or_field, Field):
if params:
raise ValueError('construct_field() cannot accept parameters when passing in a construct_field object.')
return name_or_field
# "text", analyzer="snowball"
return Field.get_dsl_class(name_or_field)(**params)
class Field(DslBase):
_type_name = 'field'
_type_shortcut = staticmethod(construct_field)
# all fields can be multifields
_param_defs = {'fields': {'type': 'field', 'hash': True}}
name = None
_coerce = False
def __init__(self, *args, **kwargs):
self._multi = kwargs.pop('multi', False)
self._required = kwargs.pop('required', False)
super(Field, self).__init__(*args, **kwargs)
def __getitem__(self, subfield):
return self._params.get('fields', {})[subfield]
def _serialize(self, data):
return data
def _deserialize(self, data):
return data
def _empty(self):
return None
def empty(self):
if self._multi:
return AttrList([])
return self._empty()
def serialize(self, data):
if isinstance(data, (list, AttrList)):
return list(map(self._serialize, data))
return self._serialize(data)
def deserialize(self, data):
if isinstance(data, (list, AttrList)):
data[:] = [
None if d is None else self._deserialize(d)
for d in data
]
return data
if data is None:
return None
return self._deserialize(data)
def clean(self, data):
if data is not None:
data = self.deserialize(data)
if data in (None, [], {}) and self._required:
raise ValidationException("Value required for this field.")
return data
def to_dict(self):
d = super(Field, self).to_dict()
name, value = d.popitem()
value['type'] = name
return value
class CustomField(Field):
name = 'custom'
_coerce = True
def to_dict(self):
if isinstance(self.builtin_type, Field):
return self.builtin_type.to_dict()
d = super(CustomField, self).to_dict()
d['type'] = self.builtin_type
return d
class Object(Field):
name = 'object'
_coerce = True
def __init__(self, doc_class=None, **kwargs):
self._doc_class = doc_class
if doc_class is None:
# FIXME import
from .document import InnerDoc
# no InnerDoc subclass, creating one instead...
self._doc_class = type('InnerDoc', (InnerDoc, ), {})
for name, field in iteritems(kwargs.pop('properties', {})):
self._doc_class._doc_type.mapping.field(name, field)
if 'dynamic' in kwargs:
self._doc_class._doc_type.mapping.meta('dynamic', kwargs.pop('dynamic'))
self._mapping = self._doc_class._doc_type.mapping
super(Object, self).__init__(**kwargs)
def __getitem__(self, name):
return self._mapping[name]
def __contains__(self, name):
return name in self._mapping
def _empty(self):
return self._wrap({})
def _wrap(self, data):
return self._doc_class.from_es(data)
def empty(self):
if self._multi:
return AttrList([], self._wrap)
return self._empty()
def to_dict(self):
d = self._mapping.to_dict()
_, d = d.popitem()
d["type"] = self.name
return d
def _collect_fields(self):
return self._mapping.properties._collect_fields()
def _deserialize(self, data):
# don't wrap already wrapped data
if isinstance(data, self._doc_class):
return data
if isinstance(data, AttrDict):
data = data._d_
return self._wrap(data)
def _serialize(self, data):
if data is None:
return None
# somebody assigned raw dict to the field, we should tolerate that
if isinstance(data, collections.Mapping):
return data
return data.to_dict()
def clean(self, data):
data = super(Object, self).clean(data)
if data is None:
return None
if isinstance(data, (list, AttrList)):
for d in data:
d.full_clean()
else:
data.full_clean()
return data
def update(self, other):
if not isinstance(other, Object):
# not an inner/nested object, no merge possible
return
self._mapping.update(other._mapping)
class Nested(Object):
name = 'nested'
def __init__(self, *args, **kwargs):
kwargs.setdefault('multi', True)
super(Nested, self).__init__(*args, **kwargs)
class Date(Field):
name = 'date'
_coerce = True
def __init__(self, *args, **kwargs):
self._default_timezone = kwargs.pop('default_timezone', None)
if isinstance(self._default_timezone, string_types):
self._default_timezone = tz.gettz(self._default_timezone)
super(Date, self).__init__(*args, **kwargs)
def _deserialize(self, data):
if isinstance(data, string_types):
try:
data = parser.parse(data)
except Exception as e:
raise ValidationException('Could not parse date from the value (%r)' % data, e)
if isinstance(data, datetime):
if self._default_timezone and data.tzinfo is None:
data = data.replace(tzinfo=self._default_timezone)
return data
if isinstance(data, date):
return data
if isinstance(data, int):
# Divide by a float to preserve milliseconds on the datetime.
return datetime.utcfromtimestamp(data / 1000.0)
raise ValidationException('Could not parse date from the value (%r)' % data)
class Text(Field):
_param_defs = {
'fields': {'type': 'field', 'hash': True},
'analyzer': {'type': 'analyzer'},
'search_analyzer': {'type': 'analyzer'},
'search_quote_analyzer': {'type': 'analyzer'},
}
name = 'text'
class Keyword(Field):
_param_defs = {
'fields': {'type': 'field', 'hash': True},
'search_analyzer': {'type': 'analyzer'},
'normalizer': {'type': 'normalizer'}
}
name = 'keyword'
class Boolean(Field):
name = 'boolean'
_coerce = True
def _deserialize(self, data):
if data == "false":
return False
return bool(data)
def clean(self, data):
if data is not None:
data = self.deserialize(data)
if data is None and self._required:
raise ValidationException("Value required for this field.")
return data
class Float(Field):
name = 'float'
_coerce = True
def _deserialize(self, data):
return float(data)
class HalfFloat(Float):
name = 'half_float'
class ScaledFloat(Float):
name = 'scaled_float'
def __init__(self, scaling_factor, *args, **kwargs):
super(ScaledFloat, self).__init__(scaling_factor=scaling_factor, *args, **kwargs)
class Double(Float):
name = 'double'
class Integer(Field):
name = 'integer'
_coerce = True
def _deserialize(self, data):
return int(data)
class Byte(Integer):
name = 'byte'
class Short(Integer):
name = 'short'
class Long(Integer):
name = 'long'
class Ip(Field):
name = 'ip'
_coerce = True
def _deserialize(self, data):
# the ipaddress library for pypy, python2.5 and 2.6 only accepts unicode.
return ipaddress.ip_address(unicode(data))
def _serialize(self, data):
if data is None:
return None
return str(data)
class Binary(Field):
name = 'binary'
_coerce = True
def _deserialize(self, data):
return base64.b64decode(data)
def _serialize(self, data):
if data is None:
return None
return base64.b64encode(data)
class GeoPoint(Field):
name = 'geo_point'
class GeoShape(Field):
name = 'geo_shape'
class Completion(Field):
name = 'completion'
class Percolator(Field):
name = 'percolator'
class IntegerRange(Field):
name = 'integer_range'
class FloatRange(Field):
name = 'float_range'
class LongRange(Field):
name = 'long_range'
class DoubleRange(Field):
name = 'double_ranged'
class DateRange(Field):
name = 'date_range'
class Join(Field):
name = 'join'
class TokenCount(Field):
name = 'token_count'
class Murmur3(Field):
name = 'murmur3'