Skip to content

Commit

Permalink
Cache serialized version of Attribute, handle cache failures, legibility
Browse files Browse the repository at this point in the history
  • Loading branch information
Anthony Lukach committed Jul 12, 2017
1 parent 76bdf43 commit 40119ba
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 4 deletions.
2 changes: 1 addition & 1 deletion jsonattrs/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.1.22'
__version__ = '0.1.23'
30 changes: 27 additions & 3 deletions jsonattrs/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections import OrderedDict
import logging
import re

from django.db import models
Expand All @@ -12,6 +13,9 @@
from django.contrib.postgres.fields import JSONField


logger = logging.getLogger(__name__)


def schema_cache_key(content_type, selectors):
return ('jsonattrs:schema:' +
content_type.app_label + ',' + content_type.model + ':' +
Expand Down Expand Up @@ -105,8 +109,11 @@ def save(self, *args, **kwargs):
def compose_schemas(*schemas):
key = 'jsonattrs:compose:' + ','.join([str(s.pk) for s in schemas])
cached = caches['jsonattrs'].get(key)
if cached is not None:
return cached
if cached:
s_attrs, required_attrs, default_attrs = cached
# Deserialize attrs when retrieving from cache
attrs = OrderedDict((k, Attribute(**v)) for k, v in s_attrs.items())
return attrs, required_attrs, default_attrs

# Extract schema attributes, names of required attributes and
# names of attributes with defaults, composing schemas.
Expand All @@ -123,7 +130,12 @@ def compose_schemas(*schemas):
default_attrs = {n for n, a in attrs.items()
if a.default is not None and a.default != ''}

caches['jsonattrs'].set(key, (attrs, required_attrs, default_attrs))
try:
# Serialize attrs to make it smaller in cache
s_attrs = OrderedDict((k, v.to_dict()) for k, v in attrs.items())
caches['jsonattrs'].set(key, (s_attrs, required_attrs, default_attrs))
except:
logger.exception("Failed to cache %r", key)
return attrs, required_attrs, default_attrs


Expand Down Expand Up @@ -251,6 +263,12 @@ class Meta:

objects = AttributeManager()

def __str__(self):
return "<Attribute #{0.id}: name={0.name}>".format(self)

def __repr__(self):
return str(self)

@property
def long_name(self):
if self.long_name_xlat is None or isinstance(self.long_name_xlat, str):
Expand Down Expand Up @@ -344,3 +362,9 @@ def render(self, val):
return ', '.join(result)
else:
return self.choice_dict.get(val, val)

def to_dict(self):
return {
k: v for k, v in self.__dict__.items()
if k in [f.attname for f in self._meta.fields]
}
133 changes: 133 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
from collections import OrderedDict
from django.test import TestCase
from unittest.mock import patch, MagicMock

from jsonattrs.models import Schema, Attribute, AttributeType, compose_schemas
from .fixtures import create_fixtures


class ComposeSchemaTest(TestCase):
def setUp(self):
self.fixtures = create_fixtures(do_schemas=False, load_attr_types=True)
self.schema = Schema.objects.create(
content_type=self.fixtures['party_t'], selectors=()
)
self.attr_type = AttributeType.objects.get(name='select_one')

@patch('jsonattrs.models.logger')
@patch('jsonattrs.models.caches')
def test_compose_schemas_cache_failure(self, mock_caches, mock_logger):
mock_get = MagicMock(return_value=None)
mock_set = MagicMock(side_effect=Exception("Failed to cache"))
mock_cache = MagicMock(set=mock_set, get=mock_get)
mock_caches.__getitem__.return_value = mock_cache
testattrs = (
testattr0, testattr1, testattr2, testattr3, testattr4,
testattr5, testattr6, testattr7, testattr8, testattr9
) = Attribute.objects.bulk_create(Attribute(
id=i,
schema_id=self.schema.id,
name='testattr%s' % i,
long_name='Test attribute%s' % i,
index=i,
attr_type_id=self.attr_type.id,
required=bool(i % 2),
default=i**i if bool(i % 3) else '') for i in range(1, 11)
)
attrs, required_attrs, default_attrs = compose_schemas(self.schema)
sid = self.schema.id
mock_logger.exception.assert_called_once_with(
'Failed to cache %r', 'jsonattrs:compose:{}'.format(sid))
assert attrs == OrderedDict((attr.name, attr) for attr in testattrs)
assert required_attrs == {
'testattr3', 'testattr5', 'testattr1', 'testattr9', 'testattr7'}
assert default_attrs == {
'testattr5', 'testattr1', 'testattr8', 'testattr4', 'testattr2',
'testattr7', 'testattr10'}

@patch('jsonattrs.models.caches')
def test_compose_schemas_cache_deserialize(self, mock_caches):
attr1, attr2 = Attribute.objects.bulk_create(Attribute(
id=i,
schema_id=self.schema.id,
name='testattr%s' % i,
long_name='Test attribute%s' % i,
index=i,
attr_type_id=self.attr_type.id,
required=bool(i % 2),
default=i**i if not bool(i % 2) else '') for i in range(1, 3)
)
cache_value = (
OrderedDict([
('testattr1', attr1.to_dict()),
('testattr2', attr2.to_dict())
]),
{'testattr1'},
{'testattr2'}
)
mock_cache = MagicMock(get=MagicMock(return_value=cache_value))
mock_caches.__getitem__.return_value = mock_cache

assert compose_schemas(self.schema) == (
OrderedDict([
('testattr1', attr1),
('testattr2', attr2)
]),
{'testattr1'},
{'testattr2'}
)
assert not mock_cache.set.called

@patch('jsonattrs.models.caches')
def test_compose_schemas_cache_serialize(self, mock_caches):
mock_cache = MagicMock(get=MagicMock(return_value=None))
mock_caches.__getitem__.return_value = mock_cache

attr1, attr2 = Attribute.objects.bulk_create(Attribute(
id=i,
schema_id=self.schema.id,
name='testattr%s' % i,
long_name='Test attribute%s' % i,
index=i,
attr_type_id=self.attr_type.id,
required=bool(i % 2),
default=i**i if not bool(i % 2) else '') for i in range(1, 3)
)
attr1.refresh_from_db()
attr2.refresh_from_db()

assert compose_schemas(self.schema) == (
OrderedDict([
('testattr1', attr1),
('testattr2', attr2)
]),
{'testattr1'},
{'testattr2'}
)
# import pudb; pudb.set_trace()
mock_cache.set.assert_called_once_with(
'jsonattrs:compose:{}'.format(self.schema.id),
(
OrderedDict([
('testattr1', attr1.to_dict()),
('testattr2', attr2.to_dict())
]),
{'testattr1'},
{'testattr2'}
)
)

def test_serialize_deserialize(self):
attr = Attribute.objects.create(
schema_id=self.schema.id,
name='testattr',
long_name='Test attribute',
index=1,
attr_type_id=self.attr_type.id,
)
assert Attribute(**attr.to_dict()) == attr

def test_str(self):
attr = Attribute(name='testattr', id=123)
assert str(attr) == '<Attribute #123: name=testattr>'
assert repr(attr) == '<Attribute #123: name=testattr>'

0 comments on commit 40119ba

Please sign in to comment.