diff --git a/dirty_models/fields.py b/dirty_models/fields.py index 58454d2..6261f3b 100644 --- a/dirty_models/fields.py +++ b/dirty_models/fields.py @@ -616,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() diff --git a/dirty_models/model_types.py b/dirty_models/model_types.py index 3f20017..cd4ca72 100644 --- a/dirty_models/model_types.py +++ b/dirty_models/model_types.py @@ -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 diff --git a/dirty_models/models.py b/dirty_models/models.py index 45c5b5a..351a801 100644 --- a/dirty_models/models.py +++ b/dirty_models/models.py @@ -8,11 +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 dirty_models.utils import underscore_to_camel -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'] @@ -92,6 +91,8 @@ class CamelCaseMeta(DirtyModelMeta): """ 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) diff --git a/dirty_models/utils.py b/dirty_models/utils.py index efeaa9b..3380fab 100644 --- a/dirty_models/utils.py +++ b/dirty_models/utils.py @@ -1,8 +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 the underscored string to camel case. + 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) diff --git a/tests/dirty_models/tests_fields.py b/tests/dirty_models/tests_fields.py index 7c24212..ce69477 100644 --- a/tests/dirty_models/tests_fields.py +++ b/tests/dirty_models/tests_fields.py @@ -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): diff --git a/tests/dirty_models/tests_utils.py b/tests/dirty_models/tests_utils.py index de6ae36..298f80d 100644 --- a/tests/dirty_models/tests_utils.py +++ b/tests/dirty_models/tests_utils.py @@ -1,6 +1,12 @@ +from datetime import datetime, date, timedelta +from json import dumps, loads from unittest.case import TestCase -from dirty_models.utils import underscore_to_camel +from dirty_models.fields import StringIdField, IntegerField, DateTimeField, ArrayField, MultiTypeField, ModelField, \ + HashMapField, DateField, TimedeltaField +from dirty_models.models import BaseModel +from dirty_models.utils import underscore_to_camel, ModelFormatterIter, ListFormatterIter, HashMapFormatterIter, \ + JSONEncoder class UnderscoreToCamelTests(TestCase): @@ -19,3 +25,97 @@ def test_underscore_number(self): def test_underscore_multi_number(self): self.assertEqual(underscore_to_camel('foo_bar_tor_pir_1'), 'fooBarTorPir_1') + + +class TestModel(BaseModel): + + test_string_field_1 = StringIdField(name='other_field') + test_int_field_1 = IntegerField() + test_datetime = DateTimeField(parse_format="%Y-%m-%dT%H:%M:%S") + test_array_datetime = ArrayField(field_type=DateTimeField(parse_format="%Y-%m-%dT%H:%M:%S")) + test_array_multitype = ArrayField(field_type=MultiTypeField(field_types=[IntegerField(), + DateTimeField( + parse_format="%Y-%m-%dT%H:%M:%S" + )])) + test_model_field_1 = ArrayField(field_type=ArrayField(field_type=ModelField())) + test_hash_map = HashMapField(field_type=DateField(parse_format="%Y-%m-%d date")) + test_timedelta = TimedeltaField() + + +class ModelFormatterIterTests(TestCase): + + def test_model_formatter(self): + + model = TestModel(data={'test_string_field_1': 'foo', + 'test_int_field_1': 4, + 'test_datetime': datetime(year=2016, month=5, day=30, + hour=22, minute=22, second=22), + 'test_array_datetime': [datetime(year=2015, month=5, day=30, + hour=22, minute=22, second=22), + datetime(year=2015, month=6, day=30, + hour=22, minute=22, second=22)], + 'test_array_multitype': [datetime(year=2015, month=5, day=30, + hour=22, minute=22, second=22), + 4, 5], + 'test_model_field_1': [[{'test_datetime': datetime(year=2015, month=7, day=30, + hour=22, minute=22, second=22)}]], + 'test_hash_map': {'foo': date(year=2015, month=7, day=30)}, + 'test_timedelta': timedelta(seconds=32.1122)}) + + formatter = ModelFormatterIter(model) + data = {k: v for k, v in formatter} + self.assertEqual(data['other_field'], 'foo') + self.assertEqual(data['test_int_field_1'], 4) + self.assertEqual(data['test_datetime'], '2016-05-30T22:22:22') + self.assertIsInstance(data['test_array_datetime'], ListFormatterIter) + self.assertEqual(list(data['test_array_datetime']), ['2015-05-30T22:22:22', '2015-06-30T22:22:22']) + self.assertIsInstance(data['test_array_multitype'], ListFormatterIter) + self.assertEqual(list(data['test_array_multitype']), ['2015-05-30T22:22:22', 4, 5]) + self.assertIsInstance(data['test_model_field_1'], ListFormatterIter) + self.assertIsInstance(list(data['test_model_field_1'])[0], ListFormatterIter) + self.assertEqual({k: v for k, v in list(list(data['test_model_field_1'])[0])[0]}, + {'test_datetime': '2015-07-30T22:22:22'}) + self.assertIsInstance(data['test_hash_map'], HashMapFormatterIter) + self.assertEqual({k: v for k, v in data['test_hash_map']}, {'foo': '2015-07-30 date'}) + self.assertEqual(data['test_timedelta'], 32.1122) + + +class JSONEncoderTests(TestCase): + + def test_model_json(self): + + model = TestModel(data={'test_string_field_1': 'foo', + 'test_int_field_1': 4, + 'test_datetime': datetime(year=2016, month=5, day=30, + hour=22, minute=22, second=22), + 'test_array_datetime': [datetime(year=2015, month=5, day=30, + hour=22, minute=22, second=22), + datetime(year=2015, month=6, day=30, + hour=22, minute=22, second=22)], + 'test_array_multitype': [datetime(year=2015, month=5, day=30, + hour=22, minute=22, second=22), + 4, 5], + 'test_model_field_1': [[{'test_datetime': datetime(year=2015, month=7, day=30, + hour=22, minute=22, second=22)}]], + 'test_hash_map': {'foo': date(year=2015, month=7, day=30)}, + 'test_timedelta': timedelta(seconds=32.1122)}) + + json_str = dumps(model, cls=JSONEncoder) + + data = {'other_field': 'foo', + 'test_int_field_1': 4, + 'test_datetime': '2016-05-30T22:22:22', + 'test_array_datetime': ['2015-05-30T22:22:22', + '2015-06-30T22:22:22'], + 'test_array_multitype': ['2015-05-30T22:22:22', 4, 5], + 'test_model_field_1': [[{'test_datetime': '2015-07-30T22:22:22'}]], + 'test_hash_map': {'foo': '2015-07-30 date'}, + 'test_timedelta': 32.1122} + + self.assertEqual(loads(json_str), data) + + def test_general_use_json(self): + + data = {'foo': 3, 'bar': 'str'} + json_str = dumps(data, cls=JSONEncoder) + self.assertEqual(loads(json_str), data)