Skip to content

Commit

Permalink
Merge pull request #84 from alfred82santa/release/0.7.0
Browse files Browse the repository at this point in the history
Release/0.7.0
  • Loading branch information
alfred82santa committed May 30, 2016
2 parents 7f96bcd + 577cc68 commit f8a3358
Show file tree
Hide file tree
Showing 12 changed files with 377 additions and 17 deletions.
26 changes: 26 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,38 @@ Features
- Datetime fields can use any datetime format using parser and formatter functions.
- No database dependent.
- Auto documentation using https://github.com/alfred82santa/dirty-models-sphinx
- Json encoder.
- Opensource (BSD License)

---------
Changelog
---------

Version 0.7.0
-------------

- Timedelta field
- Generic formatters
- Json encoder

.. code-block:: python
import json
from datetime import datetime
from dirty_models import BaseModel, DatetimeField
from dirty_models.utils import JSONEncoder
class ExampleModel(BaseModel):
field_datetime = DatetimeField(parse_format="%Y-%m-%dT%H:%M:%S")
model = ExampleModel(field_datetime=datetime.now())
assert json.dumps(model, cls=JSONEncoder) == '{"field_datetime": "2016-05-30T22:22:22"}'
- Auto camelCase fields metaclass


Version 0.6.3
-------------

Expand Down
34 changes: 29 additions & 5 deletions dirty_models/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
Fields to be used with dirty models.
"""

from datetime import datetime, date, time
from datetime import datetime, date, time, timedelta
from dateutil.parser import parse as dateutil_parse
from .model_types import ListModel
from collections import Mapping


__all__ = ['IntegerField', 'FloatField', 'BooleanField', 'StringField', 'StringIdField',
'TimeField', 'DateField', 'DateTimeField', 'ModelField', 'ArrayField',
'TimeField', 'DateField', 'DateTimeField', 'TimedeltaField', 'ModelField', 'ArrayField',
'HashMapField', 'BlobField', 'MultiTypeField']


Expand Down Expand Up @@ -130,9 +130,9 @@ def check_value(self, value):
return isinstance(value, float)

def can_use_value(self, value):
return isinstance(value, int) \
or (isinstance(value, str)
and value.replace('.', '', 1).isnumeric())
return isinstance(value, int) or \
(isinstance(value, str) and
value.replace('.', '', 1).isnumeric())


class BooleanField(BaseField):
Expand Down Expand Up @@ -376,6 +376,20 @@ def can_use_value(self, value):
return isinstance(value, (int, str, date, dict, list))


class TimedeltaField(BaseField):
"""It allows to use a timedelta as value in a field."""

def convert_value(self, value):
if isinstance(value, (int, float)):
return timedelta(seconds=value)

def check_value(self, value):
return type(value) is timedelta

def can_use_value(self, value):
return isinstance(value, (int, float))


class ModelField(BaseField):

"""
Expand Down Expand Up @@ -602,6 +616,16 @@ def can_use_value(self, value):
return True
return False

def get_field_type_by_value(self, value):
for ft in self._field_types:
if ft.check_value(value):
return ft
for ft in self._field_types:
if ft.can_use_value(value):
return ft

raise TypeError("Value `{0}` can not be used on field `{1}`".format(value, self.name))

@property
def field_types(self):
return self._field_types.copy()
2 changes: 1 addition & 1 deletion dirty_models/model_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Internal types for dirty models
"""
import itertools
from dirty_models.base import BaseData, InnerFieldTypeMixin
from .base import BaseData, InnerFieldTypeMixin
from functools import wraps


Expand Down
22 changes: 18 additions & 4 deletions dirty_models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
from collections import Mapping
from copy import deepcopy

from dirty_models.base import BaseData, InnerFieldTypeMixin
from dirty_models.fields import IntegerField, FloatField, BooleanField, StringField, DateTimeField
from dirty_models.model_types import ListModel
from .fields import BaseField, ModelField, ArrayField
from .base import BaseData, InnerFieldTypeMixin
from .fields import IntegerField, FloatField, BooleanField, StringField, DateTimeField, \
BaseField, ModelField, ArrayField
from .model_types import ListModel


__all__ = ['BaseModel', 'DynamicModel', 'FastDynamicModel', 'HashMapModel']
Expand Down Expand Up @@ -84,6 +84,20 @@ def prepare_field(cls, field):
pass


class CamelCaseMeta(DirtyModelMeta):

"""
Metaclass for dirty_models. Sets camel case version of field's name as default field name.
"""

def process_base_field(self, field, key):
from .utils import underscore_to_camel

if not field.name:
field.name = underscore_to_camel(key)
super(CamelCaseMeta, self).process_base_field(field, key)


def recover_model_from_data(model_class, original_data, modified_data, deleted_data):
"""
Function to reconstruct a model from DirtyModel basic information: original data, the modified and deleted
Expand Down
99 changes: 99 additions & 0 deletions dirty_models/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import re
from json.encoder import JSONEncoder as BaseJSONEncoder
from datetime import date, datetime, time, timedelta
from .fields import MultiTypeField, DateTimeBaseField
from .model_types import ListModel
from .models import BaseModel, HashMapModel


def underscore_to_camel(string):
"""
Converts underscored string to camel case.
"""
return re.sub('_([a-z])', lambda x: x.group(1).upper(), string)


class BaseFormatterIter:
pass


class BaseFieldtypeFormatterIter(BaseFormatterIter):

def __init__(self, obj, field, parent_formatter):
self.obj = obj
self.field = field
self.parent_formatter = parent_formatter


class ListFormatterIter(BaseFieldtypeFormatterIter):

def __iter__(self):
for item in self.obj:
yield self.parent_formatter.format_field(self.field, item)


class HashMapFormatterIter(BaseFieldtypeFormatterIter):

def __iter__(self):
for fieldname in self.obj.get_fields():
value = self.obj.get_field_value(fieldname)
yield fieldname, self.parent_formatter.format_field(self.field, value)


class BaseModelFormatterIter(BaseFormatterIter):
"""
Base formatter iterator for Dirty Models.
"""

def __init__(self, model):
self.model = model

def __iter__(self):
fields = self.model.get_fields()
for fieldname in fields:
field = self.model.get_field_obj(fieldname)
yield field.name, self.format_field(field,
self.model.get_field_value(fieldname))

def format_field(self, field, value):
if isinstance(field, MultiTypeField):
return self.format_field(field.get_field_type_by_value(value), value)
elif isinstance(value, HashMapModel):
return HashMapFormatterIter(obj=value, field=value.get_field_type(), parent_formatter=self)
elif isinstance(value, BaseModel):
return self.__class__(value)
elif isinstance(value, ListModel):
return ListFormatterIter(obj=value, field=value.get_field_type(), parent_formatter=self)

return value


class ModelFormatterIter(BaseModelFormatterIter):

"""
Iterate over model fields formatting them.
"""

def format_field(self, field, value):
if isinstance(value, (date, datetime, time)) and \
isinstance(field, DateTimeBaseField):
return field.get_formatted_value(value)
elif isinstance(value, timedelta):
return value.total_seconds()

return super(ModelFormatterIter, self).format_field(field, value)


class JSONEncoder(BaseJSONEncoder):

"""
Json encoder for Dirty Models
"""

def default(self, obj):
if isinstance(obj, BaseModel):
return {k: v for k, v in ModelFormatterIter(obj)}
elif isinstance(obj, (HashMapFormatterIter, ModelFormatterIter)):
return {k: v for k, v in obj}
elif isinstance(obj, ListFormatterIter):
return list(obj)
4 changes: 2 additions & 2 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@
# built documents.
#
# The short X.Y version.
version = '0.6.3'
version = '0.7.0'
# The full version, including alpha/beta/rc tags.
release = '0.6.3'
release = '0.7.0'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
3 changes: 2 additions & 1 deletion docs/source/dirty_models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ Dirty Models API
models
fields
inner_models
base
base
utils
7 changes: 7 additions & 0 deletions docs/source/utils.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Utilities
=========

.. automodule:: dirty_models.utils
:members:
:show-inheritance:
:no-undoc-members:
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
name='dirty-models',
url='https://github.com/alfred82santa/dirty-models',
author='alfred82santa',
version='0.6.3',
version='0.7.0',
author_email='alfred82santa@gmail.com',
classifiers=[
'Intended Audience :: Developers',
Expand Down
43 changes: 41 additions & 2 deletions tests/dirty_models/tests_fields.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from unittest import TestCase
from dirty_models.fields import (IntegerField, StringField, BooleanField,
FloatField, ModelField, TimeField, DateField,
DateTimeField, ArrayField, StringIdField, HashMapField, MultiTypeField)
DateTimeField, ArrayField, StringIdField, HashMapField, MultiTypeField, TimedeltaField)
from dirty_models.models import BaseModel, HashMapModel
from dirty_models.model_types import ListModel

from datetime import time, date, datetime, timezone
from datetime import time, date, datetime, timezone, timedelta
import iso8601


Expand Down Expand Up @@ -1448,6 +1448,18 @@ def test_update_integer_field(self):
self.model.multi_field = 3
self.assertEqual(self.model.multi_field, 3)

def test_get_field_type_by_value(self):
multi_field = MultiTypeField(field_types=[IntegerField(), (ArrayField, {"field_type": StringField()})])
self.assertIsInstance(multi_field.get_field_type_by_value(['foo', 'bar']),
ArrayField)
self.assertIsInstance(multi_field.get_field_type_by_value(3),
IntegerField)

def test_get_field_type_by_value_fail(self):
multi_field = MultiTypeField(field_types=[IntegerField(), (ArrayField, {"field_type": StringField()})])
with self.assertRaises(TypeError):
multi_field.get_field_type_by_value({})


class TestAutoreferenceModel(TestCase):

Expand All @@ -1467,3 +1479,30 @@ def test_model_reference(self):
self.assertIsInstance(self.model.multi_field[0], self.model.__class__)
self.assertIsInstance(self.model.multi_field[1], self.model.__class__)
self.assertIsInstance(self.model.array_of_array[0][0], self.model.__class__)


class TestTimedeltaField(TestCase):

def setUp(self):
self.field = TimedeltaField()

def test_check_value_success(self):
self.assertTrue(self.field.check_value(timedelta(seconds=0)))

def test_check_value_fail(self):
self.assertFalse(self.field.check_value(12))

def test_can_use_value_int(self):
self.assertTrue(self.field.can_use_value(12))

def test_can_use_value_float(self):
self.assertTrue(self.field.can_use_value(12.11))

def test_can_use_value_fail(self):
self.assertFalse(self.field.can_use_value('test'))

def test_convert_value_int(self):
self.assertTrue(self.field.convert_value(12), timedelta(seconds=12))

def test_convert_value_float(self):
self.assertTrue(self.field.convert_value(12.11), timedelta(seconds=12, milliseconds=110))
31 changes: 30 additions & 1 deletion tests/dirty_models/tests_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from dirty_models.fields import (BaseField, IntegerField, FloatField,
StringField, DateTimeField, ModelField,
ArrayField, BooleanField, DateField, TimeField, HashMapField)
from dirty_models.models import BaseModel, DynamicModel, HashMapModel, FastDynamicModel
from dirty_models.models import BaseModel, DynamicModel, HashMapModel, FastDynamicModel, CamelCaseMeta

INITIAL_DATA = {
'testField1': 'testValue1',
Expand Down Expand Up @@ -1386,3 +1386,32 @@ def test_modify_model(self):
'field_integer': 4,
'field_string': 'test',
'field_time': time(13, 56, 59)})


class CamelCaseMetaclassTest(TestCase):

def test_camelcase_fields(self):

class TestModel(BaseModel, metaclass=CamelCaseMeta):

test_field_1 = StringField()
test_field2 = StringField()
testField_3 = StringField()
testField4 = StringField(name='test_field_4')

model = TestModel(data={'testField_1': 'foo',
'testField2': 'bar',
'testField_3': 'tor',
'test_field_4': 'pir'})

self.assertEqual(model.test_field_1, 'foo')
self.assertEqual(model.testField_1, 'foo')
self.assertEqual(model.test_field2, 'bar')
self.assertEqual(model.testField2, 'bar')
self.assertEqual(model.testField_3, 'tor')
self.assertEqual(model.testField4, 'pir')
self.assertEqual(model.test_field_4, 'pir')
self.assertEqual(model.export_data(), {'testField_1': 'foo',
'testField2': 'bar',
'testField_3': 'tor',
'test_field_4': 'pir'})

0 comments on commit f8a3358

Please sign in to comment.