├── .gitignore ├── LICENCE.rst ├── README.rst ├── django_commands ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── app_db_tables_reset.py │ │ ├── cache_clear.py │ │ ├── db_backup.py │ │ ├── db_clear.py │ │ └── db_load.py ├── models.py └── utils.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | -------------------------------------------------------------------------------- /LICENCE.rst: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Mart Sõmermaa 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-commands 2 | =============== 3 | 4 | ``django-commands`` contains the following command extensions 5 | for the Django web framework: 6 | 7 | - ``db_clear`` -- removes all tables from the database, 8 | - ``db_backup`` -- creates a backup dump file of the database, 9 | - ``db_load`` -- loads data from a backup dump file to the database, 10 | - ``app_db_tables_reset`` -- deletes all data related to the given app 11 | from the database, 12 | - ``cache_clear`` -- calls ``clear_from_cache()`` for all objects 13 | in given models. 14 | 15 | Only ``app_db_tables_reset`` has been tested with Django 1.9, other commands 16 | may or may not work. 17 | 18 | Installation 19 | ------------ 20 | 21 | Install the package with pip:: 22 | 23 | $ pip install git+http://github.com/mrts/django-commands.git 24 | 25 | and add ``'django_commands'`` to ``INSTALLED_APPS`` in your Django 26 | project settings file:: 27 | 28 | INSTALLED_APPS = ( 29 | ... 30 | 'django_commands', 31 | ) 32 | 33 | Invoke ``./manage.py help`` to verify that the commands are available 34 | and ``./manage.py help commandname`` for more specific usage instructions. 35 | 36 | Intended use 37 | ------------ 38 | 39 | The commands have been created for automating remote deployments with Fabric_. 40 | 41 | See `example fabfile`_ and `project setup guidelines`_. 42 | 43 | The workflow would be as follows: 44 | 45 | - add a feature or fix a bug on git branch ``devel`` 46 | - deploy to remote staging server:: 47 | 48 | fab -H user@host:port deploy:stage 49 | 50 | - when client is happy with the change, merge it to ``master`` 51 | - deploy to remote production server:: 52 | 53 | fab -H user@host:port deploy:live 54 | 55 | - fetch database content and uploaded files from remote server as needed:: 56 | 57 | fab -H user@host:port fetch_data:live 58 | 59 | .. _Fabric: http://fabfile.org 60 | .. _example fabfile: http://gist.github.com/768913 61 | .. _project setup guidelines: http://github.com/mrts/django-commands/wiki/Proper-setup-of-a-Django-project 62 | -------------------------------------------------------------------------------- /django_commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrts/django-commands/4ece06da7ff0c5720b9d27263ab5616f747227cb/django_commands/__init__.py -------------------------------------------------------------------------------- /django_commands/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrts/django-commands/4ece06da7ff0c5720b9d27263ab5616f747227cb/django_commands/management/__init__.py -------------------------------------------------------------------------------- /django_commands/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrts/django-commands/4ece06da7ff0c5720b9d27263ab5616f747227cb/django_commands/management/commands/__init__.py -------------------------------------------------------------------------------- /django_commands/management/commands/app_db_tables_reset.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import AppCommand, CommandError 2 | from django.utils.six.moves import input 3 | from django.db import DEFAULT_DB_ALIAS, connections 4 | 5 | class Command(AppCommand): 6 | help = ( 7 | 'Removes ALL DATA related to the given app from the database ' 8 | 'by calling model.objects.all().delete() for all app models. ' 9 | 'This also removes related data in other apps via cascade.' 10 | ) 11 | 12 | def add_arguments(self, parser): 13 | super(Command, self).add_arguments(parser) 14 | parser.add_argument( 15 | '--noinput', '--no-input', 16 | action='store_false', dest='interactive', default=True, 17 | help='Tells Django to NOT prompt the user for input of any kind.', 18 | ) 19 | parser.add_argument( 20 | '--database', action='store', dest='database', default=DEFAULT_DB_ALIAS, 21 | help='Nominates a database to reset. Defaults to the "default" database.', 22 | ) 23 | 24 | def handle_app_config(self, app_config, **options): 25 | app_label = app_config.label 26 | database = options['database'] 27 | interactive = options['interactive'] 28 | db_name = connections[database].settings_dict['NAME'] 29 | 30 | confirm = (ask_confirmation(app_label, db_name) 31 | if interactive else 'yes') 32 | 33 | if confirm == 'yes': 34 | for model in app_config.get_models(): 35 | model.objects.using(database).all().delete() 36 | self.stdout.write('Reset done.\n') 37 | else: 38 | self.stdout.write("Reset cancelled.\n") 39 | 40 | def ask_confirmation(app_label, db_name): 41 | return input("""You have requested a reset of the application {app_label}. 42 | This will IRREVERSIBLY DESTROY all data related to the app currently in 43 | the {db_name} database, and return each table to empty state. 44 | Are you sure you want to do this? 45 | Type 'yes' to continue, or 'no' to cancel: """.format(**locals())) 46 | -------------------------------------------------------------------------------- /django_commands/management/commands/cache_clear.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import CommandError, LabelCommand 2 | from django.utils.datastructures import SortedDict 3 | from django_commands.utils import parse_apps_and_models, get_model_cls 4 | 5 | class Command(LabelCommand): 6 | args = ' [appname.Model] ...>' 7 | help = ("Clears object-related data from cache by calling " 8 | "clear_from_cache() for \nall objects in the model " 9 | ".") 10 | 11 | def handle_label(self, label, **options): 12 | for appname, modelname in parse_apps_and_models(label): 13 | model = get_model_cls(appname, modelname) 14 | for obj in model.objects.all(): 15 | try: 16 | obj.clear_from_cache() 17 | except AttributeError: 18 | # obj doesn't have clear_from_cache() 19 | raise CommandError("Object `%s` of model `%s.%s` " 20 | "does not have a `clear_from_cache()` method" % 21 | (obj, appname, modelname)) 22 | except Exception, e: 23 | raise CommandError("%s occurred: %s" % 24 | (e.__class__.__name__, e)) 25 | -------------------------------------------------------------------------------- /django_commands/management/commands/db_backup.py: -------------------------------------------------------------------------------- 1 | # Inspired by http://djangosnippets.org/snippets/823/ 2 | import time, os 3 | from optparse import make_option 4 | 5 | from django.core.management.base import CommandError, LabelCommand 6 | 7 | from django_commands.utils import get_db_conf, build_mysql_args, build_postgres_args 8 | 9 | class Command(LabelCommand): 10 | args = '' 11 | help = ("Creates a backup dump of the database. The " 12 | "argument \nis used for the actual backup file, " 13 | "but a timestamp and appropriate \nfile extension will be " 14 | "appended, e.g. -2000-12-31-2359.sqlite.gz.\n" 15 | "BEWARE OF SHELL INJECTION in database settings.") 16 | option_list = LabelCommand.option_list + ( 17 | make_option('--database', action='store', dest='database', 18 | help='Target database. Defaults to the "default" database.'), 19 | ) 20 | 21 | def handle_label(self, label, **options): 22 | db_conf = get_db_conf(options) 23 | 24 | backup_handler = getattr(self, '_backup_%s_db' % db_conf['engine']) 25 | ret, outfile = backup_handler(db_conf, "%s-%s" % 26 | (label, time.strftime('%Y-%m-%d-%H%M'))) 27 | 28 | if not ret: 29 | print ("Database '%s' successfully backed up to: %s" % 30 | (db_conf['db_name'], outfile)) 31 | else: 32 | raise CommandError("Database '%s' backup to '%s' failed" % 33 | (db_conf['db_name'], outfile)) 34 | 35 | def _backup_sqlite3_db(self, db_conf, outfile): 36 | outfile = '%s.sqlite.gz' % outfile 37 | _check_writable(outfile) 38 | ret = os.system('sqlite3 %s .dump | gzip -9 > %s' % 39 | (db_conf['db_name'], outfile)) 40 | 41 | return ret, outfile 42 | 43 | def _backup_postgresql_db(self, db_conf, outfile): 44 | return self._backup_postgresql_psycopg2_db(db_conf, outfile) 45 | 46 | def _backup_postgresql_psycopg2_db(self, db_conf, outfile): 47 | passwd = ('export PGPASSWORD=%s;' % db_conf['password'] 48 | if db_conf['password'] else '') 49 | outfile = '%s.pgsql.gz' % outfile 50 | _check_writable(outfile) 51 | ret = os.system('%s pg_dump %s | gzip -9 > %s' % 52 | (passwd, build_postgres_args(db_conf), outfile)) 53 | 54 | return ret, outfile 55 | 56 | def _backup_mysql_db(self, db_conf, outfile): 57 | outfile = '%s.mysql.gz' % outfile 58 | _check_writable(outfile) 59 | ret = os.system('mysqldump %s | gzip -9 > %s' % 60 | (build_mysql_args(db_conf), outfile)) 61 | 62 | return ret, outfile 63 | 64 | # Be aware of the classic race condition here. 65 | def _check_writable(filename): 66 | if os.path.exists(filename): 67 | raise CommandError("'%s' already exists, won't overwrite." % filename) 68 | dir_path = os.path.dirname(filename) 69 | if not os.access(dir_path, os.W_OK): 70 | raise CommandError("Directory '%s' is not writable." % dir_path) 71 | 72 | # or, more stringent: 73 | # from __future__ import with_statement 74 | # try: 75 | # with open(filename, 'w'): pass 76 | # except Exception, e: 77 | # raise CommandError("Cannot open '%s' for writing: %s %s" % 78 | # (filename, e.__class__.__name__, e)) 79 | -------------------------------------------------------------------------------- /django_commands/management/commands/db_clear.py: -------------------------------------------------------------------------------- 1 | from optparse import make_option 2 | 3 | from django.core.management.base import CommandError, NoArgsCommand 4 | 5 | class Command(NoArgsCommand): 6 | help = "Removes tables from the database." 7 | option_list = NoArgsCommand.option_list + ( 8 | make_option('--noinput', action='store_false', dest='interactive', 9 | default=True, help='Do not ask the user for confirmation before ' 10 | 'clearing.'), 11 | make_option('--all-tables', action='store_true', dest='all_tables', 12 | default=False, help='Removes all tables, not only the ones ' 13 | 'managed by Django.'), 14 | make_option('--database', action='store', dest='database', 15 | help='Target database. Defaults to the "default" database.'), 16 | ) 17 | 18 | def handle_noargs(self, **options): 19 | from django.conf import settings 20 | from django import VERSION 21 | 22 | if VERSION[:2] < (1, 2): 23 | from django.db import connection 24 | dbname = settings.DATABASE_NAME 25 | else: 26 | from django.db import connections, DEFAULT_DB_ALIAS 27 | db_alias = options.get('database') or DEFAULT_DB_ALIAS 28 | connection = connections[db_alias] 29 | dbname = connection.settings_dict['NAME'] 30 | 31 | if _confirm(options, dbname) == 'yes': 32 | _drop_tables(connection, dbname, options['all_tables']) 33 | else: 34 | print "Cancelled." 35 | 36 | 37 | def _confirm(options, dbname): 38 | if not options['interactive']: 39 | return 'yes' 40 | 41 | message = ("You have requested to drop %s tables in the '%s' " 42 | "database. \nThis will IRREVERSIBLY DESTROY the data.\n" 43 | "Are you sure you want to do this?\n" 44 | "Type 'yes' to continue, or any other value to cancel: " % 45 | (('all' if options['all_tables'] else 'Django-managed'), dbname)) 46 | 47 | return raw_input(message) 48 | 49 | def _drop_tables(connection, dbname, all_tables): 50 | from django.db import transaction 51 | 52 | tables = (connection.introspection.table_names() if all_tables else 53 | connection.introspection.django_table_names(only_existing=True)) 54 | qn = connection.ops.quote_name 55 | # use CASCADE with all backends except SQLite 56 | sql_template = ('DROP TABLE %s;' if 'sqlite' in str(connection) else 57 | 'DROP TABLE %s CASCADE;') 58 | drop_table_sql = (sql_template % qn(table) for table in tables) 59 | 60 | try: 61 | cursor = connection.cursor() 62 | for sql in drop_table_sql: 63 | cursor.execute(sql) 64 | except Exception, e: 65 | transaction.rollback_unless_managed() 66 | raise CommandError("""Database '%s' couldn't be flushed. 67 | %s occurred: %s 68 | The full SQL: \n%s""" % 69 | (dbname, e.__class__.__name__, e, "\n".join(drop_table_sql))) 70 | 71 | transaction.commit_unless_managed() 72 | -------------------------------------------------------------------------------- /django_commands/management/commands/db_load.py: -------------------------------------------------------------------------------- 1 | import os 2 | from optparse import make_option 3 | 4 | from django.core.management.base import CommandError, LabelCommand 5 | 6 | from django_commands.utils import get_db_conf, build_mysql_args, build_postgres_args 7 | 8 | class Command(LabelCommand): 9 | args = '' 10 | help = "Loads data from a backup dump file to the database." 11 | option_list = LabelCommand.option_list + ( 12 | make_option('--noinput', action='store_false', dest='interactive', 13 | default=True, help='Do not ask the user for confirmation before ' 14 | 'clearing.'), 15 | make_option('--database', action='store', dest='database', 16 | help='Target database. Defaults to the "default" database.'), 17 | ) 18 | 19 | def handle_label(self, label, **options): 20 | if not os.access(label, os.R_OK): 21 | raise CommandError("File '%s' is not readable." % label) 22 | 23 | db_conf = get_db_conf(options) 24 | 25 | if _confirm(options['interactive'], db_conf['db_name']) == 'yes': 26 | load_handler = getattr(self, '_load_%s_db' % db_conf['engine']) 27 | ret = load_handler(db_conf, label) 28 | if not ret: 29 | print ("Data from '%s' was successfully loaded to '%s'" % 30 | (label, db_conf['db_name'])) 31 | else: 32 | raise CommandError("Loading data from '%s' to '%s' failed" % 33 | (label, db_conf['db_name'])) 34 | else: 35 | print "Cancelled." 36 | 37 | def _load_sqlite3_db(self, db_conf, infile): 38 | return os.system('zcat %s | sqlite3 %s' % (infile, db_conf['db_name'])) 39 | 40 | def _load_postgresql_db(self, db_conf, infile): 41 | return self._load_postgresql_psycopg2_db(db_conf, infile) 42 | 43 | def _load_postgresql_psycopg2_db(self, db_conf, infile): 44 | passwd = ('export PGPASSWORD=%s;' % db_conf['password'] 45 | if db_conf['password'] else '') 46 | return os.system('%s zcat %s | psql %s -f -' % 47 | (passwd, infile, build_postgres_args(db_conf))) 48 | 49 | def _load_mysql_db(self, db_conf, infile): 50 | return os.system('zcat %s | mysqldump %s' % 51 | (infile, build_mysql_args(db_conf))) 52 | 53 | 54 | def _confirm(interactive, dbname): 55 | if not interactive: 56 | return 'yes' 57 | return raw_input("You have requested to load data to the '%s' " 58 | "database. \nThis will IRREVERSIBLY DESTROY existing data.\n" 59 | "Are you sure you want to do this?\n" 60 | "Type 'yes' to continue, or any other value to cancel: " % dbname) 61 | -------------------------------------------------------------------------------- /django_commands/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrts/django-commands/4ece06da7ff0c5720b9d27263ab5616f747227cb/django_commands/models.py -------------------------------------------------------------------------------- /django_commands/utils.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import CommandError 2 | 3 | def get_db_conf(options): 4 | from django.conf import settings 5 | from django import VERSION 6 | 7 | if VERSION[:2] < (1, 2): 8 | return { 9 | 'engine': settings.DATABASE_ENGINE, 10 | 'db_name': settings.DATABASE_NAME, 11 | 'user': settings.DATABASE_USER, 12 | 'password': settings.DATABASE_PASSWORD, 13 | 'host': settings.DATABASE_HOST, 14 | 'port': settings.DATABASE_PORT, 15 | } 16 | else: 17 | from django.db import DEFAULT_DB_ALIAS 18 | db_alias = options.get('database') or DEFAULT_DB_ALIAS 19 | return { 20 | 'engine': (settings.DATABASES[db_alias]['ENGINE'] 21 | .rsplit('.', 1)[-1]), 22 | 'db_name': settings.DATABASES[db_alias]['NAME'], 23 | 'user': settings.DATABASES[db_alias]['USER'], 24 | 'password': settings.DATABASES[db_alias]['PASSWORD'], 25 | 'host': settings.DATABASES[db_alias]['HOST'], 26 | 'port': settings.DATABASES[db_alias]['PORT'], 27 | } 28 | 29 | def build_postgres_args(db_conf): 30 | args = [] 31 | if db_conf['user']: 32 | args.append("--username=%s" % db_conf['user']) 33 | if db_conf['host']: 34 | args.append("--host=%s" % db_conf['host']) 35 | if db_conf['port']: 36 | args.append("--port=%s" % db_conf['port']) 37 | args.append(db_conf['db_name']) 38 | 39 | return ' '.join(args) 40 | 41 | def build_mysql_args(db_conf): 42 | args = ["--%s=%s" % (arg, db_conf[arg]) for arg in 43 | 'user', 'password', 'host', 'port' if db_conf[arg]] 44 | args.append(db_conf['db_name']) 45 | 46 | return ' '.join(args) 47 | 48 | def parse_apps_and_models(label): 49 | apps_and_models = [] 50 | for chunk in label.split(): 51 | try: 52 | app, model = chunk.split('.', 1) 53 | except ValueError: 54 | raise CommandError("Invalid arguments: %s" % label) 55 | apps_and_models.append((app, model)) 56 | return apps_and_models 57 | 58 | def get_model_cls(appname, modelname): 59 | from django.db.models import get_model 60 | try: 61 | model = get_model(appname, modelname) 62 | except Exception, e: 63 | raise CommandError("%s occurred: %s" % 64 | (e.__class__.__name__, e)) 65 | if not model: 66 | raise CommandError("Unknown model: %s.%s" % 67 | (appname, modelname)) 68 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from setuptools import setup, find_packages 4 | 5 | version = "0.0.2" 6 | 7 | setup( 8 | name = 'django-commands', 9 | version = version, 10 | description="Database and cache management command extensions for the Django web framework", 11 | author="Mart Sõmermaa", 12 | author_email="mrts.pydev at gmail dot com", 13 | url="http://github.com/mrts/django-commands", 14 | license="MIT", 15 | packages=find_packages(exclude=['tests']), 16 | zip_safe=False, 17 | ) 18 | --------------------------------------------------------------------------------