Read only Django shell

Say you have a bunch of developers that occasionally need Django shell access to production, but you want this to be an exceptional event. Here is a drop-in replacement for ./ shell that defaults to read-only mode, but lets the developer switch to writable mode on the fly, while notifying the team.

import sys
import os
from optparse import make_option
from import Command as DjangoShellCommand
from django.db import router
from import NoArgsCommand
from myapp.utils import hipchat

_original_db_for_write = None

def confirm_writable(self, *args, **kwargs):
    ''' user can over-ride the shell to be writable at any time, but it sends a message '''

    # for migrations, you might be in a non-interactive shell
    # so don't prompt, but still send out the notification
    if sys.stdin.isatty():
        cont = raw_input('Are you sure you want to connect to the production database in writable mode? [y/N] ')
        if not cont.lower().startswith('y'):
            raise IOError('Database in read-only mode.')

    router.db_for_write = _original_db_for_write

def send_alert():
    hipchat.send_message("I'm opening up a writable prod shell!",

class Command(DjangoShellCommand):

    option_list = DjangoShellCommand.option_list + (
        make_option('--write', action='store_true', dest='writable',
            help='Connect to the database in writable mode.'),

    def handle_noargs(self, **options):

        # only allow read-only shells in prod by default
        if options.get('writable'):
            global _original_db_for_write
            _original_db_for_write = router.db_for_write
            router.db_for_write = confirm_writable

        return super(Command, self).handle_noargs(**options)

The strategy here is to use Django's database router mechanism to throw an exception when trying to write to the database.


Drop this into your project as myapp/management/commands/ and it will over-ride the default shell command.


In my case, I'm notifying the team via Hipchat. Of course, you can replace this function with a version that sends out an email, etc. If you're curious, here is the hipchat code:

import json
import urllib
import urllib2

def _make_hipchat_request(url, auth_token=None):
    if not auth_token:
        from django.conf import settings
        auth_token = settings.HIPCHAT_TOKEN
    final_url = "%s%s%sauth_token=%s" % (
        '&' if '?' in url else '?',
    return urllib2.urlopen(final_url).read()

def send_message(message, room_id='Engineering', from_name='Django', auth_token=None, color='yellow'):
    url = '/rooms/message?message=%s&room_id=%s&from=%s&message_format=text&color=%s' % (
    _make_hipchat_request(url, auth_token)

I'm currently working at NerdWallet, a startup in San Francisco trying to bring clarity to all of life's financial decisions. We're hiring like crazy. Hit me up on Twitter, I would love to talk.

Follow @chase_seibert on Twitter