Skip to content
This repository has been archived by the owner on Apr 27, 2020. It is now read-only.

Commit

Permalink
Add an example application with dummy sqla model to run tests against.
Browse files Browse the repository at this point in the history
Add an sqla specific extention with hook on session events to get model modifications.
  • Loading branch information
hadrien committed Apr 27, 2014
1 parent 7d8b397 commit 10f062a
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 25 deletions.
10 changes: 4 additions & 6 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ Development
* Add a highly not efficient implementation of a key versioner
* Add a model versioner which depends on key versioner and model identity
inspector
* TODO:

* A pyramid example app with dummy sqla model;
* sqla specific extention with hook on session events to get model
modifications.
* Plumbing components
* Add an example application with dummy sqla model to run tests against
* Add an sqla specific extention with hook on session events to get model
modifications.
TODO:
* Tests, tests and tests
5 changes: 5 additions & 0 deletions example/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@


def includeme(config):
config.include('pyramid_caching')
config.include('.model')
43 changes: 43 additions & 0 deletions example/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@

from sqlalchemy import Column, Integer, String, MetaData, ForeignKey, Text
from sqlalchemy.engine import engine_from_config
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship

metadata = MetaData()

Base = declarative_base(metadata=metadata)
Session = sessionmaker()


def includeme(config):
config.include('pyramid_caching.ext.sqlalchemy')
engine = engine_from_config(config.registry.settings)
metadata.create_all(engine)
Session.configure(bind=engine)

config.register_sqla_session_caching_hook(Session)
config.register_sqla_base_class(Base)


class User(Base):

__tablename__ = 'user'

id = Column('id', Integer, primary_key=True)

name = Column('name', String(32))

notes = relationship('UserNote', backref='user', cascade='delete')


class UserNote(Base):

__tablename__ = 'user_note'

user_id = Column('user_id', ForeignKey('user.id'),
primary_key=True)

id = Column('id', Integer, primary_key=True, autoincrement=True)

content = Column('name', Text())
4 changes: 4 additions & 0 deletions pyramid_caching/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@


def includeme(config):
config.include('.versioners')
Empty file added pyramid_caching/ext/__init__.py
Empty file.
75 changes: 75 additions & 0 deletions pyramid_caching/ext/sqlalchemy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from __future__ import absolute_import
import inspect

from zope.interface import implementer

from pyramid_caching.interfaces import IIdentityInspector

from sqlalchemy import event


def includeme(config):

config.add_directive('register_sqla_session_caching_hook',
register_sqla_session_caching_hook)

config.add_directive('register_sqla_base_class',
register_sqla_base_class)

identity_inspector = SqlAlchemyIdentityInspector()

config.registry.registerUtility(identity_inspector)


def register_sqla_base_class(config, base_cls):
registry = config.registry

identity_inspector = registry.getUtility(IIdentityInspector)

def identify(model):
return identity_inspector.identify(model)

registry.registerAdapter(identify, required=[base_cls],
provided=IIdentityInspector)

registry.registerAdapter(identify, required=[base_cls.__class__],
provided=IIdentityInspector)


def register_sqla_session_caching_hook(config, session_cls):
model_versioner = config.get_model_versioner()

def on_before_commit(session):
dirty = session.dirty

def incr_models(session):
# XXX: should increment only cacheable models
for model in dirty:
model_versioner.incr(model)

if dirty:
event.listen(session_cls, 'after_commit', incr_models)

event.listen(session_cls, 'before_commit', on_before_commit)


@implementer(IIdentityInspector)
class SqlAlchemyIdentityInspector(object):

def identify(self, obj_or_cls):
tablename = obj_or_cls.__tablename__

if inspect.isclass(obj_or_cls):
return tablename

# with a table user_message with a composite primary key user_id and id
# an object user_message(user_id=123, id=456) will give:
# 'user_message:user_id=123:id=456'

# TODO: if table has no primary keys :-/

table = obj_or_cls.__table__
ids = ':'.join(['%s=%s' % (col_name, getattr(obj_or_cls, col_name))
for col_name in table.primary_key.columns.keys()])

return '%s:%s' % (obj_or_cls.__tablename__, ids)
32 changes: 32 additions & 0 deletions pyramid_caching/tests/functional/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import unittest

import webtest

from pyramid.config import Configurator
from pyramid.decorator import reify


class Base(unittest.TestCase):

@reify
def config(self):
self.addCleanup(delattr, self, 'config')
_config = Configurator(settings={
'sqlalchemy.url': 'sqlite:///:memory:',
})
_config.include('example')
_config.commit()
return _config

@reify
def app(self):
self.addCleanup(delattr, self, 'app')
return webtest.TestApp(self.config.make_wsgi_app())

@property
def registry(self): # pragma no cover
return self.config.registry

@property
def model_versioner(self):
return self.config.get_model_versioner()
88 changes: 88 additions & 0 deletions pyramid_caching/tests/functional/test_sqlachemy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from pyramid.decorator import reify
from . import Base


class TestSqlAlchemyExt(Base):

def setUp(self):
self.app

def tearDown(self):
from example.model import User
for user in self.sqla_session.query(User).all():
self.sqla_session.delete(user)
self.sqla_session.commit()

@reify
def sqla_session(self):
from example.model import Session
session = Session()
self.addCleanup(session.close)
self.addCleanup(delattr, self, 'sqla_session')
return session

def test_incr(self):
from example.model import User
user = User()

key_cls_0 = self.model_versioner.get_key(User)
key_obj_0 = self.model_versioner.get_key(user)

self.model_versioner.incr(user)

key_cls_1 = self.model_versioner.get_key(User)
key_obj_1 = self.model_versioner.get_key(user)

self.assertNotEqual(key_cls_1, key_cls_0)
self.assertNotEqual(key_obj_1, key_obj_0)

def test_unicity_simple_pk(self):
from example.model import User

user0 = User(name=u'Bob Marley')
user1 = User(name=u'Peter Tosh')

self.sqla_session.add(user0)
self.sqla_session.add(user1)
self.sqla_session.commit()

self.assertNotEqual(
self.model_versioner.get_key(user0),
self.model_versioner.get_key(user1),
)

def test_unicity_composite_pk(self):
from example.model import User, UserNote

msg0 = UserNote(id=11, content='I ray')
msg1 = UserNote(id=22, content='Jah')
user = User(name=u'Bob Marley')

user.notes = [msg0, msg1]

self.sqla_session.add(user)
self.sqla_session.commit()

self.assertNotEqual(
self.model_versioner.get_key(msg0),
self.model_versioner.get_key(msg1),
)

def test_auto_incr(self):
from example.model import User, UserNote

user = User(name=u'Bob')

self.sqla_session.add(user)

self.sqla_session.commit()

key_user_v0 = self.model_versioner.get_key(user)

user.name = 'Bob Marley'

self.sqla_session.commit()

key_user_v1 = self.model_versioner.get_key(user)

self.assertNotEqual(key_user_v0, key_user_v1)
2 changes: 1 addition & 1 deletion pyramid_caching/tests/unittests/test_versioners.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def instantiate_model():

key_versioner = MemoryKeyVersioner()
model_versioner = ModelVersioner(key_versioner,
BasicModelIdentityInspector())
BasicModelIdentityInspector().identify)
return key_versioner, model_versioner, instantiate_model, BasicModel


Expand Down
47 changes: 29 additions & 18 deletions pyramid_caching/versioners.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,29 @@

from zope.interface import implementer

from pyramid_caching.interfaces import IKeyVersioner, IModelVersioner
from pyramid_caching.interfaces import (
IIdentityInspector,
IKeyVersioner,
IModelVersioner,
)


def includeme(config):
config.registerAdapter()
registry = config.registry

def identify(model_obj_or_cls):
return registry.queryAdapter(model_obj_or_cls, IIdentityInspector)

key_versioner = MemoryKeyVersioner()

model_versioner = ModelVersioner(key_versioner, identify)

config.registry.registerUtility(model_versioner)
config.add_directive('get_model_versioner', get_model_versioner)


def get_model_versioner(config):
return config.registry.getUtility(IModelVersioner)


@implementer(IKeyVersioner)
Expand All @@ -21,7 +39,7 @@ def __init__(self):
self.versions = dict()

def _format(self, key):
return 'version.' + key
return 'version.%s' % key

def get(self, key, default=0):
return self.versions.get(self._format(key), default)
Expand All @@ -38,33 +56,26 @@ def incr(self, key, start=0):
@implementer(IModelVersioner)
class ModelVersioner(object):

def __init__(self, key_versioner, identity_inspector):
def __init__(self, key_versioner, identify):
self.key_versioner = key_versioner
self.identity_inspector = identity_inspector
self.identify = identify

def get_key(self, obj_or_cls):
identity = self.identity_inspector.identify(obj_or_cls)
return '%s.v_%s' % (identity, self.key_versioner.get(identity))
identity = self.identify(obj_or_cls)
return '%s:v=%s' % (identity, self.key_versioner.get(identity))

def get_multi_keys(self, objects_or_classes):
keys = [self.identity_inspector.identify(obj_or_cls)
keys = [self.identify(obj_or_cls)
for obj_or_cls in objects_or_classes]

versions = self.key_versioner.get_multi(keys)

return ['%s.v_%s' % (key, version)
return ['%s:v=%s' % (key, version)
for (key, version) in zip(keys, versions)]

def incr(self, obj_or_cls, start=0):
self.key_versioner.incr(self.identity_inspector.identify(obj_or_cls))
self.key_versioner.incr(self.identify(obj_or_cls))

if not inspect.isclass(obj_or_cls): # increment model class version
identity = self.identity_inspector.identify(obj_or_cls.__class__)
identity = self.identify(obj_or_cls.__class__)
self.key_versioner.incr(identity)

def on_model_changes(self, models):
for model in models:
self.incr(model)

def on_model_change(self, model):
self.incr(model)

0 comments on commit 10f062a

Please sign in to comment.