commit 53f8078a55888e7e1b3dc47d98ffe12f406b1c30 Author: Bertrand Bordage Date: Fri Sep 26 16:53:44 2014 +0200 Version 0.1.0. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af2e6c9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.pyc +*.so +build/ +dist/ +sdist/ +*.egg-info/ diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 0000000..b3999f0 --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,6 @@ +Authors +======= + +================ ========================== +Bertrand Bordage bordage.bertrand@gmail.com +================ ========================== diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f818d46 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2014, Bertrand Bordage +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of django-cachalot nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..d4f84ca --- /dev/null +++ b/README.rst @@ -0,0 +1,39 @@ +Django-cachalot +=============== + +Caches your Django ORM queries and automatically invalidates them. + +**In alpha, do not use for production** + +.. image:: https://raw.github.com/BertrandBordage/django-cachalot/master/django-cachalot.jpg + + +Quick start +----------- + +Requirements +............ + +Django-cachalot currently requires Django 1.6 +and `django-redis `_ as your default +cache backend. It should work with both Python 2 & 3. + +Usage +..... + +#. `pip install -e git+https://github.com/BertrandBordage/django-cachalot#egg=django-cachalot` +#. Add ``'cachalot',`` to your ``INSTALLED_APPS`` +#. Enjoy! + + +What still needs to be done +--------------------------- + +- Correctly invalidate ``.extra`` queries +- Handle transactions +- Handle multiple database +- Write tests, including multi-table inheritance, prefetch_related, etc +- Find out if it’s thread-safe and test it +- Add a ``CACHALOT_ENABLED`` setting +- Add a setting to choose a cache other than ``'default'`` +- Add support for other caches like memcached diff --git a/cachalot/__init__.py b/cachalot/__init__.py new file mode 100644 index 0000000..c1abbca --- /dev/null +++ b/cachalot/__init__.py @@ -0,0 +1,2 @@ +__version__ = (0, 1, 0) +version_string = '.'.join(str(n) for n in __version__) diff --git a/cachalot/models.py b/cachalot/models.py new file mode 100644 index 0000000..cb5220b --- /dev/null +++ b/cachalot/models.py @@ -0,0 +1,5 @@ +from .monkey_patch import is_patched, monkey_patch_orm + + +if not is_patched(): + monkey_patch_orm() diff --git a/cachalot/monkey_patch.py b/cachalot/monkey_patch.py new file mode 100644 index 0000000..c28cea2 --- /dev/null +++ b/cachalot/monkey_patch.py @@ -0,0 +1,101 @@ +# coding: utf-8 + +from __future__ import unicode_literals +from collections import Iterable +from django.core.cache import cache +from django.db.models.query import EmptyResultSet +from django.db.models.sql.compiler import ( + SQLCompiler, SQLAggregateCompiler, SQLDateCompiler, SQLDateTimeCompiler, + SQLInsertCompiler, SQLUpdateCompiler, SQLDeleteCompiler) + + +COMPILERS = (SQLCompiler, + SQLAggregateCompiler, SQLDateCompiler, SQLDateTimeCompiler, + SQLInsertCompiler, SQLUpdateCompiler, SQLDeleteCompiler) +WRITE_COMPILERS = (SQLInsertCompiler, SQLUpdateCompiler, SQLDeleteCompiler) +READ_COMPILERS = [c for c in COMPILERS if c not in WRITE_COMPILERS] + + +PATCHED = False +MISS_VALUE = '[[The cache key was missed]]' + + +def _get_tables_cache_keys(compiler): + q = compiler.query + # FIXME: `.extra` (and maybe more) are not in alias_map + tables = q.alias_map.keys() + + return ['%s_queries' % t for t in tables] + + +def _update_tables_queries(compiler, cache_key): + tables_cache_keys = _get_tables_cache_keys(compiler) + tables_queries = cache.get_many(tables_cache_keys) + for k in tables_cache_keys: + queries = tables_queries.get(k, []) + queries.append(cache_key) + tables_queries[k] = queries + cache.set_many(tables_queries) + + +def _invalidate_tables(compiler): + tables_cache_keys = _get_tables_cache_keys(compiler) + tables_queries = cache.get_many(tables_cache_keys) + queries = [] + for k in tables_cache_keys: + queries.extend(tables_queries.get(k, [])) + cache.delete_many(queries) + cache.delete_many(tables_cache_keys) + + +def _monkey_patch_orm_read(): + def patch_execute_sql(method): + def inner(compiler, *args, **kwargs): + if isinstance(compiler, WRITE_COMPILERS): + return method(compiler, *args, **kwargs) + + try: + cache_key = compiler.as_sql() + except EmptyResultSet: + return method(compiler, *args, **kwargs) + + result = cache.get(cache_key, MISS_VALUE) + + if result == MISS_VALUE: + result = method(compiler, *args, **kwargs) + if isinstance(result, Iterable) \ + and not isinstance(result, (tuple, list)): + result = list(result) + + _update_tables_queries(compiler, cache_key) + + cache.set(cache_key, result) + + return result + + return inner + + for compiler in READ_COMPILERS: + compiler.execute_sql = patch_execute_sql(compiler.execute_sql) + + +def _monkey_patch_orm_write(): + def patch_execute_sql(method): + def inner(compiler, *args, **kwargs): + _invalidate_tables(compiler) + return method(compiler, *args, **kwargs) + return inner + + for compiler in WRITE_COMPILERS: + compiler.execute_sql = patch_execute_sql(compiler.execute_sql) + + +def monkey_patch_orm(): + global PATCHED + _monkey_patch_orm_write() + _monkey_patch_orm_read() + PATCHED = True + + +def is_patched(): + return PATCHED diff --git a/django-cachalot.jpg b/django-cachalot.jpg new file mode 100644 index 0000000..09b61c3 Binary files /dev/null and b/django-cachalot.jpg differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d08919c --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Django>=1.6 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..a07f309 --- /dev/null +++ b/setup.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + +import os +from setuptools import setup, find_packages +from cachalot import version_string + + +CURRENT_PATH = os.path.abspath(os.path.dirname(__file__)) + +with open(os.path.join(CURRENT_PATH, 'requirements.txt')) as f: + required = f.read().splitlines() + + +setup( + name='django-cachalot', + version=version_string, + author='Bertrand Bordage', + author_email='bordage.bertrand@gmail.com', + url='https://github.com/BertrandBordage/django-cachalot', + description='Caches your Django ORM queries ' + 'and automatically invalidates them.', + long_description=open('README.rst').read(), + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Topic :: Internet :: WWW/HTTP', + ], + license='BSD', + packages=find_packages(), + install_requires=required, + include_package_data=True, + zip_safe=False, +)