Skip to content

Commit

Permalink
Merge pull request #69 from alfred82santa/feature/fast-dynamic-model
Browse files Browse the repository at this point in the history
Feature/fast dynamic model
  • Loading branch information
alfred82santa committed Sep 15, 2014
2 parents d9a37fe + 1609d46 commit e8b9a1f
Show file tree
Hide file tree
Showing 7 changed files with 350 additions and 64 deletions.
47 changes: 47 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Features
- Easy import from/export to dict.
- Basic field type implemented.
- HashMap model. It could be used instead of DynamicModel.
- FastDynamicModel. It could be used instead of DynamicModel. Same behavior better performance.
- Pickable models.
- Datetime fields can use any datetime format using parser and formatter functions.
- No database dependent.
Expand Down Expand Up @@ -87,3 +88,49 @@ Basic usage
Look at tests for more examples


*****************
Performance Tests
*****************

.. code-block:: bash
$ python3 performancerunner.py
DynamicModel start
DynamicModel: iteration no. 0 start
DynamicModel: iteration no. 0 => 0:00:02.528166
DynamicModel: iteration no. 1 start
DynamicModel: iteration no. 1 => 0:00:03.415274
DynamicModel: iteration no. 2 start
DynamicModel: iteration no. 2 => 0:00:03.115128
DynamicModel: iteration no. 3 start
DynamicModel: iteration no. 3 => 0:00:04.091488
DynamicModel: iteration no. 4 start
DynamicModel: iteration no. 4 => 0:00:05.275302
DynamicModel => 0:00:18.425358
FastDynamicModel start
FastDynamicModel: iteration no. 0 start
FastDynamicModel: iteration no. 0 => 0:00:01.351796
FastDynamicModel: iteration no. 1 start
FastDynamicModel: iteration no. 1 => 0:00:01.265681
FastDynamicModel: iteration no. 2 start
FastDynamicModel: iteration no. 2 => 0:00:01.270142
FastDynamicModel: iteration no. 3 start
FastDynamicModel: iteration no. 3 => 0:00:01.273443
FastDynamicModel: iteration no. 4 start
FastDynamicModel: iteration no. 4 => 0:00:01.280512
FastDynamicModel => 0:00:06.441574
BlobField start
BlobField: iteration no. 0 start
BlobField: iteration no. 0 => 0:00:00.000082
BlobField: iteration no. 1 start
BlobField: iteration no. 1 => 0:00:00.000027
BlobField: iteration no. 2 start
BlobField: iteration no. 2 => 0:00:00.000025
BlobField: iteration no. 3 start
BlobField: iteration no. 3 => 0:00:00.000024
BlobField: iteration no. 4 start
BlobField: iteration no. 4 => 0:00:00.000023
BlobField => 0:00:00.000181
{'DynamicModel': {'results': [datetime.timedelta(0, 2, 528166), datetime.timedelta(0, 3, 415274), datetime.timedelta(0, 3, 115128), datetime.timedelta(0, 4, 91488), datetime.timedelta(0, 5, 275302)], 'total': datetime.timedelta(0, 18, 425358)}, 'FastDynamicModel': {'results': [datetime.timedelta(0, 1, 351796), datetime.timedelta(0, 1, 265681), datetime.timedelta(0, 1, 270142), datetime.timedelta(0, 1, 273443), datetime.timedelta(0, 1, 280512)], 'total': datetime.timedelta(0, 6, 441574)}, 'BlobField': {'results': [datetime.timedelta(0, 0, 82), datetime.timedelta(0, 0, 27), datetime.timedelta(0, 0, 25), datetime.timedelta(0, 0, 24), datetime.timedelta(0, 0, 23)], 'total': datetime.timedelta(0, 0, 181)}}
177 changes: 138 additions & 39 deletions dirty_models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def delete_field_value(self, name):
if name in self._modified_data:
self._modified_data.pop(name)

if name in self._original_data:
if name in self._original_data and name not in self._deleted_fields:
self._deleted_fields.append(name)

def reset_field_value(self, name):
Expand Down Expand Up @@ -454,58 +454,31 @@ def reset_attr_by_path(self, field):
self.reset_field_value(field)


class DynamicModel(BaseModel):
class BaseDynamicModel(BaseModel):

"""
DynamicModel allow to create model with no structure. Each instance has its own
derivated class from DynamicModels.
"""

_next_id = 0

def __new__(cls, *args, **kwargs):
new_class = type('DynamicModel_' + str(cls._next_id), (cls,), {})
cls._next_id = id(new_class)
return super(DynamicModel, new_class).__new__(new_class)

def __setattr__(self, name, value):
if not self.__hasattr__(name):
if not self.get_read_only() or not self.is_locked():
field_type = self._get_field_type(name, value)
if not field_type:
return
setattr(self.__class__, name, field_type)
super(DynamicModel, self).__setattr__(name, value)
"""
_dynamic_model = None

def __getattr__(self, name):
return self.get_field_value(name)

def __hasattr__(self, name):
try:
getattr(super(DynamicModel, self), name)
return getattr(super(BaseDynamicModel, self), name)
except AttributeError:
try:
self.__dict__[name]
except KeyError:
try:
self.__class__.__dict__[name]
except KeyError:
return False
return True
return self.get_field_value(name)

def __reduce__(self):
"""
Reduce function to allow dumpable by pickle
"""
return recover_model_from_data, (DynamicModel, self.export_original_data(),
return recover_model_from_data, (self.__class__, self.export_original_data(),
self.export_modified_data(), self.export_deleted_fields(),)

def copy(self):
"""
Creates a copy of model
"""
return DynamicModel(data=self.export_data())
return self.__class__(data=self.export_data())

def _get_field_type(self, key, value):
"""
Expand All @@ -521,8 +494,8 @@ def _get_field_type(self, key, value):
return StringField(name=key)
elif isinstance(value, datetime):
return DateTimeField(name=key)
elif isinstance(value, (dict, DynamicModel)):
return ModelField(name=key, model_class=DynamicModel)
elif isinstance(value, (dict, BaseDynamicModel)):
return ModelField(name=key, model_class=self._dynamic_model or self.__class__)
elif isinstance(value, BaseModel):
return ModelField(name=key, model_class=value.__class__)
elif isinstance(value, (list, set, ListModel)):
Expand All @@ -544,6 +517,51 @@ def import_data(self, data):
setattr(self, key, value)


class DynamicModel(BaseDynamicModel):

"""
DynamicModel allow to create model with no structure. Each instance has its own
derivated class from DynamicModels.
"""

_next_id = 0

def __new__(cls, *args, **kwargs):
new_class = type('DynamicModel_' + str(cls._next_id), (cls,), {'_dynamic_model': DynamicModel})
cls._next_id = id(new_class)
return super(DynamicModel, new_class).__new__(new_class)

def __setattr__(self, name, value):
if not self.__hasattr__(name):
if not self.get_read_only() or not self.is_locked():
field_type = self._get_field_type(name, value)
if not field_type:
return
setattr(self.__class__, name, field_type)

super(DynamicModel, self).__setattr__(name, value)

def __hasattr__(self, name):
try:
getattr(super(DynamicModel, self), name)
except AttributeError:
try:
self.__dict__[name]
except KeyError:
try:
self.__class__.__dict__[name]
except KeyError:
return False
return True

def __reduce__(self):
"""
Reduce function to allow dumpable by pickle
"""
return recover_model_from_data, (DynamicModel, self.export_original_data(),
self.export_modified_data(), self.export_deleted_fields(),)


def recover_hashmap_model_from_data(model_class, original_data, modified_data, deleted_data, field_type):
"""
Function to reconstruct a model from DirtyModel basic information: original data, the modified and deleted
Expand Down Expand Up @@ -602,8 +620,13 @@ def get_validated_object(self, value):

def __setattr__(self, name, value):
if not self.__hasattr__(name) and (not self.get_read_only() or not self.is_locked()):
if value is None:
delattr(self, name)
return
validated_value = self.get_validated_object(value)
if name not in self._original_data or self._original_data[name] != validated_value:

if validated_value is not None and \
(name not in self._original_data or self._original_data[name] != validated_value):
self.set_field_value(name, validated_value)
return

Expand All @@ -624,10 +647,86 @@ def __hasattr__(self, name):
return True

def __getattr__(self, name):
return self.get_field_value(name)
try:
return getattr(super(HashMapModel, self), name)
except AttributeError:
return self.get_field_value(name)

def __delattr__(self, name):
if not self.__hasattr__(name) and (not self.get_read_only() or not self.is_locked()):
self.delete_field_value(name)
return
super(HashMapModel, self).__delattr__(name)


class FastDynamicModel(BaseDynamicModel):

"""
FastDynamicModel allow to create model with no structure.
"""

_field_types = None

def __init__(self, *args, **kwargs):
self._field_types = {}
self._dynamic_model = FastDynamicModel
super(FastDynamicModel, self).__init__(*args, **kwargs)

def _get_real_name(self, name):
new_name = super(FastDynamicModel, self)._get_real_name(name)
if not new_name:
return name
return new_name

def get_validated_object(self, field_type, value):
"""
Returns the value validated by the field_type
"""
if field_type.check_value(value) or field_type.can_use_value(value):
data = field_type.use_value(value)
self._prepare_child(data)
return data
else:
return None

def __setattr__(self, name, value):
if self._field_types is not None and not self.__hasattr__(name) \
and (not self.get_read_only() or not self.is_locked()):
if value is None:
delattr(self, name)
return
try:
field_type = self._field_types[name]
except KeyError:
field_type = self._get_field_type(name, value)
if not field_type:
return
self._field_types[name] = field_type

validated_value = self.get_validated_object(field_type, value)
if validated_value is not None and \
(name not in self._original_data or self._original_data[name] != validated_value):
self.set_field_value(name, validated_value)
return

super(FastDynamicModel, self).__setattr__(name, value)

def __hasattr__(self, name):
try:
getattr(super(FastDynamicModel, self), name)
except AttributeError:
try:
self.__dict__[name]
except KeyError:
try:
self.__class__.__dict__[name]
except KeyError:
return False

return True

def __delattr__(self, name):
if not self.__hasattr__(name) and (not self.get_read_only() or not self.is_locked()):
self.delete_field_value(name)
return
super(FastDynamicModel, self).__delattr__(name)
8 changes: 4 additions & 4 deletions performance/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ def run(self):
test = data['test_class'](**data['params'])
test.prepare()
result_test = []
print ('{0} start'.format(label))
print('{0} start'.format(label))
for i in range(data.get('repeats', 1)):
print ('{0}: iteration no. {1} start'.format(label, i))
print('{0}: iteration no. {1} start'.format(label, i))
time_start = datetime.now()
test.run()
time_stop = datetime.now()
elapsed = time_stop - time_start
print ('{0}: iteration no. {1} => {2}'.format(label, i, str(elapsed)))
print('{0}: iteration no. {1} => {2}'.format(label, i, str(elapsed)))
result_test.append(elapsed)
total = reduce(lambda acc, x: acc + x, result_test, timedelta())
print ('{0} => {1}'.format(label, str(total)))
print('{0} => {1}'.format(label, str(total)))
result[label] = {'results': result_test, 'total': total}

return result
25 changes: 25 additions & 0 deletions performance/fastdynamicmodel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'''
Created on 15/09/2014
:author: alfred
'''
from dirty_models.models import BaseModel, FastDynamicModel
from performance.dynamicmodel import create_dict
from dirty_models.fields import ModelField


class FakeDynModel(BaseModel):
fake_data = ModelField(model_class=FastDynamicModel)


class FastDynamicModelPerformance:

def __init__(self, depth=5, children_count=5):
self.depth = depth
self.children_count = children_count

def prepare(self):
self.data = create_dict(self.depth, self.children_count)

def run(self):
return FakeDynModel(data={'fake_data': self.data})
8 changes: 6 additions & 2 deletions performancerunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@
from performance import Runner
from performance.dynamicmodel import DynamicModelPerformance
from performance.blobfield import BlobFieldPerformance
from performance.fastdynamicmodel import FastDynamicModelPerformance

config = {'DynamicModel': {'test_class': DynamicModelPerformance,
'repeats': 5,
'params': {'depth': 6, 'children_count': 6}},
'BlobField': {'test_class': BlobFieldPerformance,
'repeats': 5,
'params': {'depth': 6, 'children_count': 6}}}
'repeats': 5,
'params': {'depth': 6, 'children_count': 6}},
'FastDynamicModel': {'test_class': FastDynamicModelPerformance,
'repeats': 5,
'params': {'depth': 6, 'children_count': 6}}}

if __name__ == '__main__':

Expand Down
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.3.6',
version='0.4.0',
author_email='alfred82santa@gmail.com',
classifiers=[
'Intended Audience :: Developers',
Expand Down

0 comments on commit e8b9a1f

Please sign in to comment.