Skip to content

Commit

Permalink
Implemented new GeoFields and added Validators support
Browse files Browse the repository at this point in the history
  • Loading branch information
rochacbruno committed Jan 1, 2016
1 parent d37fa36 commit eadeec6
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 52 deletions.
10 changes: 9 additions & 1 deletion esengine/bases/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,23 @@ def __setattr__(self, key, value):
super(BaseDocument, self).__setattr__(key, value)

def to_dict(self):
"""
Transform value from Python to Dict to be saved in E.S
:return: dict
"""
result = {}
for field_name, field_instance in self._fields.iteritems():
value = getattr(self, field_name)
field_instance.validate(field_name, value)
result.update({field_name: field_instance.to_dict(value)})
return result

@classmethod
def from_dict(cls, dct):
"""
Transform data read from E.S to Python Document Object
:param dct: Result from E.S (hits, source as dict)
:return: Instance of Document
"""
params = {}
for field_name, field_instance in cls._fields.iteritems():
serialized = dct.get(field_name)
Expand Down
40 changes: 33 additions & 7 deletions esengine/bases/field.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,66 @@
from collections import Iterable

from esengine.exceptions import RequiredField, InvalidMultiField
from esengine.exceptions import FieldTypeMismatch
from esengine.exceptions import FieldTypeMismatch, ValidationError


class BaseField(object):

def __init__(self, field_type=None, required=False, multi=False, **kwargs):
def __init__(self, field_type=None, required=False, multi=False,
field_name=None, validators=None, **kwargs):
self._validators = validators or []
self._field_name = field_name
if field_type is not None:
self._type = field_type
self._required = required or getattr(self, '_required', False)
self._multi = multi or getattr(self, '_multi', False)
for key, value in kwargs.iteritems():
setattr(self, key, value)

def validate(self, field_name, value):
def validate(self, value):
if value is None:
if self._required:
raise RequiredField(field_name)
raise RequiredField(self._field_name)
else:
if self._multi:
if not isinstance(value, Iterable):
raise InvalidMultiField(field_name)
raise InvalidMultiField(self._field_name)
for elem in value:
if not isinstance(elem, self._type):
raise FieldTypeMismatch(field_name, self._type,
raise FieldTypeMismatch(self._field_name, self._type,
elem.__class__)
else:
if not isinstance(value, self._type):
raise FieldTypeMismatch(field_name, self._type,
raise FieldTypeMismatch(self._field_name, self._type,
value.__class__)
for validator in self._validators:
"""
Functions in self._validators receives field_name, value
should return None or
raise Exception (ValidationError) or return any value
"""
print self._validators # noqa
val = validator(self._field_name, value)
if val:
raise ValidationError(
'Invalid %s, returned: %s' % (self._field_name, val)
)

def to_dict(self, value):
"""
Transform value from Python to be saved in E.S
:param value: raw value
:return: pure value
"""
self.validate(value)
return value

def from_dict(self, serialized):
"""
Transform data read from E.S to Python Object
:param serialized: Result from E.S (string)
:return: Instance or Instances of self._type
"""
if serialized is not None:
if self._multi:
return [self._type(x) for x in serialized]
Expand Down
3 changes: 2 additions & 1 deletion esengine/bases/metaclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ def __new__(mcls, name, bases, attrs): # noqa
for base in bases:
if hasattr(base, '_autoid'):
if base._autoid and 'id' not in attrs:
attrs['id'] = StringField()
attrs['id'] = StringField(field_name='id')
break
for key, value in attrs.iteritems():
if isinstance(value, BaseField):
value._field_name = key
attrs['_fields'][key] = value
cls = type.__new__(mcls, name, bases, attrs)
if any(x.__name__ == 'EmbeddedDocument' for x in bases):
Expand Down
13 changes: 13 additions & 0 deletions esengine/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,19 @@ def delete_all(cls, docs, es=None, **kwargs):
]
eh.bulk(cls.get_es(es), actions, **kwargs)

@classmethod
def random(cls, size=None):
_query = {
"query": {
"function_score": {
"query": {"match_all": {}},
"random_score": {}
}
}
}
results = cls.search(_query, size=size)
return results

def __unicode__(self):
return unicode(self.__str__())

Expand Down
16 changes: 8 additions & 8 deletions esengine/embedded_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,26 @@ def to_dict(self, value):
return [self._to_dict_element(elem) for elem in value]
return self._to_dict_element(value)

def _validate_element(self, field_name, elem):
def _validate_element(self, elem):
if not isinstance(elem, EmbeddedDocument):
raise FieldTypeMismatch(field_name, self.__class__._type,
raise FieldTypeMismatch(self._field_name, self.__class__._type,
elem.__class__)
for field_name, field_class in self._fields.iteritems():
value = getattr(elem, field_name)
field_class.validate(field_name, value)
field_class.validate(value)

def validate(self, field_name, value):
def validate(self, value):
if value is None:
if self._required:
raise RequiredField(field_name)
raise RequiredField(self._field_name)
else:
if self._multi:
if not isinstance(value, Iterable):
raise InvalidMultiField(field_name)
raise InvalidMultiField(self._field_name)
for elem in value:
self._validate_element(field_name, elem)
self._validate_element(elem)
else:
self._validate_element(field_name, value)
self._validate_element(value)

def _from_dict_element(self, dct):
params = {}
Expand Down
4 changes: 4 additions & 0 deletions esengine/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ class InvalidMultiField(Exception):
pass


class ValidationError(Exception):
pass


class FieldTypeMismatch(Exception):

def __init__(self, field_name, expected_type, actual_type):
Expand Down
56 changes: 54 additions & 2 deletions esengine/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from datetime import datetime
from esengine.bases.field import BaseField
from esengine.exceptions import ValidationError


class IntegerField(BaseField):
Expand All @@ -27,8 +28,58 @@ class BooleanField(BaseField):
_type = bool


class GeoField(FloatField):
_multi = True
class GeoField(BaseField):
"""
A field to hold GeoPoint
self.mode = dict, array, string
string: A string representation, with "lat,lon"
{"location": "40.715, -74.011"}
array: An array representation with [lon,lat].
{"location": [ -73.983, 40.719 ]}
dict An object representation with lat and lon explicitly named.
{ "location": { "lat": 40.722, "lon": -73.989}}
"""

def __init__(self, *args, **kwargs):
self.mode = kwargs.pop('mode', 'dict')
super(GeoField, self).__init__(*args, **kwargs)
if self.mode == 'string':
self._type = unicode

def string_validator(field_name, value):
values = [float(item.strip()) for item in value.split(',')]
if not len(values) == 2:
raise ValidationError(
'2 elements "lat,lon" required in %s' % field_name
)

self._validators.append(string_validator)

elif self.mode == 'array':
self._multi = True
self._type = float

def array_validator(field_name, value):
if not len(value) == 2:
raise ValidationError(
'2 elements [lon, lat] required in %s' % field_name
)

self._validators.append(array_validator)

else:
self._type = dict

def dict_validator(field_name, value):
for key in 'lat', 'lon':
if not isinstance(value.get(key), float):
raise ValidationError(
'%s: %s requires a float' % (field_name, key)
)

self._validators.append(dict_validator)


class DateField(BaseField):
Expand All @@ -39,6 +90,7 @@ def _date_format(self):
return getattr(self, 'date_format', "%Y-%m-%d %H:%M:%S")

def to_dict(self, value):
self.validate(value)
if value:
return value.strftime(self._date_format)

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

setup(
name='esengine',
version="0.0.5",
version="0.0.6",
url='https://github.com/catholabs/esengine',
license='MIT',
author="Catholabs",
Expand Down
5 changes: 3 additions & 2 deletions tests/test_base_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,9 @@ class Doc(BaseDocument):
_index = 'test'
_strict = True
_fields = {
'multiple': BaseField(field_type=int, multi=True),
'simple': BaseField(field_type=int)
'multiple': BaseField(field_type=int, multi=True,
field_name='multiple'),
'simple': BaseField(field_type=int, field_name='simple')
}
doc = Doc(multiple=[1, 2], simple="10")
with pytest.raises(FieldTypeMismatch) as ex:
Expand Down
31 changes: 16 additions & 15 deletions tests/test_base_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,43 @@


def test_raise_when_required_fild_has_empty_value():
field = BaseField(required=True)
field = BaseField(required=True, field_name="test")
with pytest.raises(RequiredField) as ex:
field.validate('test', None)
field.validate(None)
assert str(ex.value) == "test"
field = BaseField(required=False)
field.validate('test', None)
field = BaseField(required=False, field_name="test")
field.validate(None)


def test_raise_when_multi_fild_is_not_iterable():
field = BaseField(field_type=int, multi=True)
field.validate('test', [10])
field = BaseField(field_type=int, multi=True, field_name="test")
field.validate([10])
with pytest.raises(InvalidMultiField) as ex:
field.validate('test', 10)
field.validate(10)
assert str(ex.value) == "test"


def test_raise_when_multi_fild_type_missmatch():
field = BaseField(field_type=int, multi=True)
field = BaseField(field_type=int, multi=True, field_name="test")
with pytest.raises(FieldTypeMismatch) as ex:
field.validate('test', [10, 'asdf'])
field.validate([10, 'asdf'])
assert str(ex.value) == "`test` expected `<type 'int'>`, actual `<type 'str'>`" # noqa


def test_raise_when_nom_iterable_is_passed_to_multi():
field = BaseField(field_type=int, required=False)
field.validate('test', 10)
field = BaseField(field_type=int, required=False, field_name="test")
field.validate(10)
with pytest.raises(FieldTypeMismatch) as ex:
field.validate('test', [10])
field.validate([10])
assert str(ex.value) == "`test` expected `<type 'int'>`, actual `<type 'list'>`" # noqa


def test_to_dict_return_same_value():
field = BaseField(field_type=int, multi=True)
x = 10
field = BaseField(field_type=int, multi=True, field_name="test")
x = [10, 11]
assert field.to_dict(x) is x
field = BaseField(field_type=int, multi=False)
field = BaseField(field_type=int, multi=False, field_name="test")
x = 10
assert field.to_dict(x) is x


Expand Down
27 changes: 13 additions & 14 deletions tests/test_embedded_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,37 +29,36 @@ def test_multi_to_dict():


def test_raise_when_validate_is_not_multi_field():
field = TowFields(multi=True)
field = TowFields(multi=True, field_name="test")
with pytest.raises(InvalidMultiField) as ex:
field.validate('test', 10)
field.validate(10)
assert str(ex.value) == "test"


def test_raise_when_validate_required_field():
field = TowFields(required=True)
field = TowFields(required=True, field_name="test")
with pytest.raises(RequiredField) as ex:
field.validate('test', None)
field.validate(None)
assert str(ex.value) == "test"


def test_validate():
field = TowFields(x=10, y=15)
field.validate('test', field)
field = TowFields(x=10, y=15, field_name="test")
field.validate(field)


def test_validate_multi():
field = TowFields(multi=True, x=10, y=15)
field.validate('test', [field, field])
field = TowFields(multi=True, x=10, y=15, field_name="test")
field.validate([field, field])


def test_raise_when_multi_fild_type_missmatch():
field = TowFields(multi=True)
field_name = 'test'
field = TowFields(multi=True, field_name="test")
with pytest.raises(FieldTypeMismatch) as ex:
field.validate(field_name, [10, 'asdf'])
assert str(ex.value) == "`{}` expected `{}`, actual `<type 'int'>`".format(
field_name,
TowFields._type
field.validate([10, 'asdf'])
tmpl = "`{field._field_name}` expected `{field._type}`, actual `<type 'int'>`"
assert str(ex.value) == tmpl.format(
field=field
)


Expand Down
Loading

0 comments on commit eadeec6

Please sign in to comment.