Permalink
Browse files

Version 0.1.0

  • Loading branch information...
0 parents commit 8517e37a150a2eca82216e9a6a377532d85ff83a @streeter committed Apr 28, 2011
Showing with 301 additions and 0 deletions.
  1. +1 −0 AUTHORS
  2. +5 −0 CHANGES
  3. +12 −0 LICENSE
  4. +2 −0 MANIFEST.in
  5. +78 −0 README.md
  6. +115 −0 readonly/__init__.py
  7. +4 −0 readonly/exceptions.py
  8. +3 −0 readonly/tests.py
  9. +40 −0 runtests.py
  10. +41 −0 setup.py
@@ -0,0 +1 @@
+http://github.com/streeter/django-db-readonly/contributors
@@ -0,0 +1,5 @@
+
+0.1.0
+* Initial implementation
+
+(History beyond 0.1.0 is not present)
12 LICENSE
@@ -0,0 +1,12 @@
+Copyright (c) 2011 Chris Streeter and individual contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+ 2. 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.
+
+ 3. Neither the name of the django-db-readonly 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.
@@ -0,0 +1,2 @@
+include setup.py README.md MANIFEST.in LICENSE
+global-exclude *~
@@ -0,0 +1,78 @@
+## About
+
+An way to globally disable writes to your database. This works by
+inserting a cursor wrapper between Django's `CursorWrapper` and
+the database connection's cursor wrapper. So many cursor wrappers!
+
+
+## Installation
+
+I uploaded it to [PyPi][pypi], so you can grab it there if you'd like with
+
+```bash
+pip install django-db-readonly
+```
+
+or install with pip the git address:
+
+```bash
+pip install git+git@github.com:streeter/django-db-readonly.git
+```
+
+You're choice. Then add `readonly` to your `INSTALLED_APPS`.
+
+
+## Usage
+
+You shouldn't notice this at all, _unless_ you add the following line
+to your `settings.py`:
+
+```python
+SITE_READ_ONLY = True
+```
+
+When you do this, any write action to your databases will generate an
+exception. You should catch this exception and deal with it somehow. Or
+let Django display an [error 500 page][error500]. The exception you will
+want to catch is `readonly.exceptions.DatabaseWriteDenied` which inherits
+from `django.db.utils.DatabaseError`.
+
+
+## Testing
+
+There aren't any tests included, yet. Run it at your own peril.
+
+
+## Caveats
+
+This will work with [Django Debug Toolbar][ddt]. In fact, I was inspired
+by [DDT's sql panel][sql-panel] when writing this app.
+
+However, in order for both DDT _and_ django-db-readonly to work, you need
+to make sure that you have `readonly` before `debug_toolbar` in your
+`INSTALLED_APPS`. Otherwise, you are responsible for debugging what is
+going on. Of course, I'm not sure why you'd be running DDT in production
+and running django-db-readonly in development, but whatever, I'm not you.
+
+More generally, if you have any other apps that modifies either
+`django.db.backends.util.CursorWrapper` or
+`django.db.backends.util.CursorDebugWrapper`, you need to make sure
+that `readonly` is placed _before_ of those apps in `INSTALLED_APPS`.
+
+
+## The Nitty Gritty
+
+How does this do what it does? Well, django-db-readonly sits between
+Django's own cursor wrapper at `django.db.backends.util.CursorWrapper` and
+the database specific cursor at `django.db.backends.*.base.*CursorWrapper`.
+It overrides two specific methods: `execute` and `executemany`. If the
+site is in read-only mode, then the SQL is examined to see if it
+contains any write actions (defined in
+`readonly.ReadOnlyCursorWrapper.SQL_WRITE_BLACKLIST`). If a write is
+detected, an exception is raised.
+
+
+[pypi]: http://pypi.python.org/pypi/django-db-readonly/
+[error500]: http://docs.djangoproject.com/en/1.3/topics/http/urls/#handler500
+[ddt]: https://github.com/robhudson/django-debug-toolbar
+[sql-panel]: https://github.com/robhudson/django-debug-toolbar/blob/master/debug_toolbar/panels/sql.py
@@ -0,0 +1,115 @@
+"""
+Django DB Readonly
+~~~~~~~~~~~~~~~~~~
+"""
+
+try:
+ VERSION = __import__('pkg_resources') \
+ .get_distribution('django-db-readonly').version
+except Exception, e:
+ VERSION = 'unknown'
+
+from time import time
+
+from django.conf import settings
+from django.db.backends import util
+from django.utils.log import getLogger
+from readonly.exceptions import DatabaseWriteDenied
+
+
+logger = getLogger('django.db.backends')
+
+
+class ReadOnlyCursorWrapper(object):
+ """
+ This is a wrapper for a database cursor.
+
+ This sits between django's own wrapper at
+ `django.db.backends.util.CursorWrapper` and the database specific cursor at
+ `django.db.backends.*.base.*CursorWrapper`. It overrides two specific
+ methods: `execute` and `executemany`. If the site is in read-only mode, then
+ the SQL is examined to see if it contains any write actions. If a write
+ is detected, an exception is raised.
+
+ A site is in read only mode by setting the SITE_READ_ONLY setting. For
+ obvious reasons, this is False by default.
+
+ Raises a DatabaseWriteDenied exception if writes are disabled.
+ """
+
+ SQL_WRITE_BLACKLIST = (
+ # Data Definition
+ 'CREATE', 'ALTER', 'RENAME', 'DROP', 'TRUNCATE',
+ # Data Manipulation
+ 'INSERT INTO', 'UPDATE', 'REPLACE', 'DELETE FROM',
+ )
+
+ def __init__(self, cursor):
+ self.cursor = cursor
+ self.readonly = getattr(settings, 'SITE_READ_ONLY', False)
+
+ def execute(self, sql, params=()):
+ # Check the SQL
+ if self.readonly and self._write_sql(sql):
+ raise DatabaseWriteDenied
+ return self.cursor.execute(sql, params)
+
+ def executemany(self, sql, param_list):
+ # Check the SQL
+ if self.readonly and self._write_sql(sql):
+ raise DatabaseWriteDenied
+ return self.cursor.executemany(sql, param_list)
+
+ def __getattr__(self, attr):
+ return getattr(self.cursor, attr)
+
+ def __iter__(self):
+ return iter(self.cursor)
+
+ def _write_sql(self, sql):
+ return sql.startswith(self.SQL_WRITE_BLACKLIST)
+
+class CursorWrapper(util.CursorWrapper):
+ def __init__(self, cursor, db):
+ self.cursor = ReadOnlyCursorWrapper(cursor)
+ self.db = db
+
+
+# Redefine CursorDebugWrapper because we want it to inherit from *our*
+# CursorWrapper instead of django.db.backends.util.CursorWrapper
+class CursorDebugWrapper(CursorWrapper):
+
+ def execute(self, sql, params=()):
+ start = time()
+ try:
+ return self.cursor.execute(sql, params)
+ finally:
+ stop = time()
+ duration = stop - start
+ sql = self.db.ops.last_executed_query(self.cursor, sql, params)
+ self.db.queries.append({
+ 'sql': sql,
+ 'time': "%.3f" % duration,
+ })
+ logger.debug('(%.3f) %s; args=%s' % (duration, sql, params),
+ extra={'duration':duration, 'sql':sql, 'params':params}
+ )
+
+ def executemany(self, sql, param_list):
+ start = time()
+ try:
+ return self.cursor.executemany(sql, param_list)
+ finally:
+ stop = time()
+ duration = stop - start
+ self.db.queries.append({
+ 'sql': '%s times: %s' % (len(param_list), sql),
+ 'time': "%.3f" % duration,
+ })
+ logger.debug('(%.3f) %s; args=%s' % (duration, sql, param_list),
+ extra={'duration':duration, 'sql':sql, 'params':param_list}
+ )
+
+# Monkey Patching!
+util.CursorWrapper = CursorWrapper
+util.CursorDebugWrapper = CursorDebugWrapper
@@ -0,0 +1,4 @@
+from django.db.utils import DatabaseError
+
+class DatabaseWriteDenied(DatabaseError):
+ pass
@@ -0,0 +1,3 @@
+"""
+Tests? What Tests?
+"""
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+import sys
+from os.path import dirname, abspath, join
+
+from django.conf import settings
+
+if not settings.configured:
+ settings.configure(
+ DATABASE_ENGINE='sqlite3',
+
+ # Uncomment below to run tests with mysql
+ #DATABASE_ENGINE='django.db.backends.mysql',
+ #DATABASE_NAME='readonly_test',
+ #DATABASE_USER='readonly_test',
+ #DATABASE_HOST='/var/mysql/mysql.sock',
+ INSTALLED_APPS=[
+ 'readonly',
+ ],
+ ROOT_URLCONF='',
+ DEBUG=False,
+ SITE_READ_ONLY=True,
+ )
+
+from django.test.simple import run_tests
+
+def runtests(*test_args):
+ if 'south' in settings.INSTALLED_APPS:
+ from south.management.commands import patch_for_test_db_setup
+ patch_for_test_db_setup()
+
+ if not test_args:
+ test_args = ['readonly']
+ parent = dirname(abspath(__file__))
+ sys.path.insert(0, parent)
+ failures = run_tests(test_args, verbosity=0, interactive=True)
+ sys.exit(failures)
+
+
+if __name__ == '__main__':
+ runtests(*sys.argv[1:])
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+
+try:
+ from setuptools import setup, find_packages
+ from setuptools.command.test import test
+except ImportError:
+ from ez_setup import use_setuptools
+ use_setuptools()
+ from setuptools import setup, find_packages
+ from setuptools.command.test import test
+
+
+class mytest(test):
+ def run(self, *args, **kwargs):
+ from runtests import runtests
+ runtests()
+ # Upgrade().run(dist=True)
+ # test.run(self, *args, **kwargs)
+
+setup(
+ name='django-db-readonly',
+ version='0.1.0',
+ author='Chris Streeter',
+ author_email='pypi@chrisstreeter.com',
+ url='http://github.com/streeter/django-db-readonly',
+ description = 'Add a global database read-only setting.',
+ packages=find_packages(),
+ zip_safe=False,
+ install_requires=[
+ ],
+ test_suite = 'readonly.tests',
+ include_package_data=True,
+ cmdclass={"test": mytest},
+ classifiers=[
+ 'Framework :: Django',
+ 'Intended Audience :: Developers',
+ 'Intended Audience :: System Administrators',
+ 'Operating System :: OS Independent',
+ 'Topic :: Software Development'
+ ],
+)

0 comments on commit 8517e37

Please sign in to comment.