INTRODUCTION
As a Django developer, one of the handiest command line (CL) utilities you will often use are the Django's manage.py
management commands. You use it to perform manifold administrative tasks like starting an app, running local host server, synchronizing database models, creating super users, and a lot of other cool tasks.
How nice would it be to add your custom commands to the manage.py
commands tailored to your administrative needs, ranging from loading data into a database, to creating new permissions and groups for your user model?
In this technical discourse, I am going to show you how you can add custom commands to the python manage.py commands. We will create a command to load
and delete
book data from our database in a book app we will create.
Prerequisites
To follow up with this tutorial without hurdles, I expect you to have:
• little experience with Django.
• little experience with the command line.
Django manage.py command
The manage.py
is a python file that Django creates whenever you start a Django project with the django-admin
command-line utility. For most cases, the django-admin —though sparingly used— and manage.py command-line utilities does the same thing.
Asides from the similarity in their uses, They both have one of the usual Command Line Interface (CLI) Syntax:
<program> <command> [options] [arguments]
django-admin <command> [options] [arguments]
manage.py <command> [options][arguments]
In the above syntax:
• The command
specifies the action the program will perform. It must either be one of the file names from the core.management.commands
folder or a name of the custom command we created ourselves.
Run
python manage.py help --commands
to display the list of allmanage.py
commands
•The options
for a given command control
how the program will execute the command. They are positional arguments ( optional or required ).
Take for instance:
python manage.py migrate --database = mydb
The above command has a command word of migrate
and an option of --database.
The whole command makes migration into the database that the --database
option specified ( mydb ) instead of the default database in our settings.py
.
The command options
modify the behavior of a command.
• The arguments
are external values you pass to the command. They are values that you give to the command to work on. For instance, the mydb database passed to the above command is an argument
.
Creating the Book App
In your terminal,
- Install Django
$pip install django
- Start a BookApp Project
This command creates a project skeleton for us. It includes$django-admin startproject BookApp
BookApp/BookApp/
folder. Change into the BookApp directory
$cd BookApp
Create a Book Application.
$python manage.py startapp Book
- Open the /BookApp/settings.py and add the
Book
app we installed to the INSTALLED_APP list:
INSTALLED_APPS = [
….
'Book'
]
After running the above commands, You should have a project tree as shown below:
Run python manage.py runserver
to start the development server at port 8000.
Click on the localhost
link — http://127.0.0.1:8000
—, you should get the custom Django welcome page that indicates the installation was successful.
Creating Model
We will create a simple book model with fields for name
, author
, and ISBN
.
We will load data into the database with the custom command that we will add to the python manage.py managements command.
Open the Book/models.py
and let create a simple model Book model with the above fields :
from django.db import models
# Create your models here
class Book(models.Model):
name = models.CharField(max_length=60)
author = models.CharField(max_length=50)
isbn = models.CharField(unique=True, max_length=50)
def __str__(self):
return self.name
In the code above:
• We import the models
class from django.db
• We created a Book
database model that inherits from models.Model
.
• We created three Char fields ( name, author, isbn ) in the book database model.
•We provided a unique =True for the isbn field so that we can’t store two different books with the same ISBN.
• We added the __str__
method for a nice representation of our book object. The method returns the name of the book object.
In the terminal, run python manage.py makemigrations
then python manage.py migrate
to create a Book table for our model in the database.
Adding Custom Command
To add a custom manage.py management command:
Add a
management/commands
directory In theBook
app folder.For every python file that does not start with an
underscore
in that directory, Django will register amanage.py
command. That is, if we create apopulate.py
file and_delete.py
, Django will only add thepopulate
command to the manage.py command list and ignore the_delete.py
file. (Source, Django docs)Create a
populate.py
file in themanagement/commands
directory.
The populate.py
python file must define a Command
class that extends Django's BaseCommand class
. These are the only requirements required to create and add the populate
command to the manage.py command list.
With the populate
command we will create, We will populate the database with the following data :
book_data = [
{
"name": "Introduction to Django",
"author": "Mustapha Ahmad",
"isbn": 1298349059
},
{
"name": "A Practical Guild to Technical Writing",
"author": "AbdulAzeez AbdulAzeez",
"isbn": 145378263
},
{
"name": "Building API with Fast API",
"author": "AbdulRahman Habeeb",
"isbn": 1293279067
},
{
"name": "Training a Machine Learning Model with Keras",
"author": "Lawal Afeez",
"isbn": 127490345
},
]
Add the data above to the populate.py python file.
Code Snippet
from django.core.management.base import BaseCommand, CommandError
from django.db.utils import IntegrityError
from Book.models import Book
class Command(BaseCommand):
help = "Populates the database with the data above"
def handle(self, *args, **options):
try:
for data in book_data:
new_book = Book.objects.create(name=data["name"],
author=data["author"],
isbn = str(data["isbn"]))
new_book.save()
except for IntegrityError:
raise CommandError(f" Book with the {data['isbn']} ISBN already exist in the book database")
self.stdout.write(self.style.SUCCESS("All book data added successfully !"))
successfully !"))
Imports
From
django.core.management.base
, We import the CommandError and the BaseCommand class that the required Command class will subclass.From django.db.utils, we import the IntegrityError that we handled in the
try and except
block whenever the UNIQUE constraint failed ( adding data that is already in the database to the database ) due to the unique requirements of the ISBN on our model. if we try to add a book with an ISBN that is already in the database, Django will generate anIntergrityError
.From the Book app, we import the
Book
database model we created earlier.
Command Class
The Command class as I said earlier, is the only requirement for this to work.
The help
variable describes what the populate
command does and it's optional.
The logic of the operation we want our command to perform will be in the function body of the handle method
. The method takes the usual class argument self
and wildcard arguments*args
and **options
.
Let's take a look at the wildcard **option argument
, We’ll come back to the handle method
a bit later.
Our command can accept both positional argument and keyword options, and aside from that, Django adds additional options like --help
to our command. All these are passed into the handle method via the wildcard **options
. The choice of the name of **options
rather than the usual **kwargs
is merely for descriptive purposes.
Back in the handle method,
We loop through the book_data with a
for loop
.We
try
to create a book data for every data in the book_data and save it.If the current data's ISBN already exists in the database, we raise a CommandError with the error message —
f" Book with the {data['isbn']} ISBN already exist in the book database"
— to the terminal in theexcept
block.If there is no error, we output a success message to the standard output ( Screen ) with the following line:
self.stdout.write(self.style.SUCCESS("All book data added successfully !"))
The command for our populate
command will be python manage.py populate
.
Open the terminal and type in the command. If you type in the codes correctly, you should see the success message ( All book data added successfully
) we specified as shown below:
If we try to run the populate command
again with the same data, we will get the error message because books with the ISBN
exist already in the database :
To confirm if our book data were created successfully;
open the python shell:
$ python manage.py shell
import the Book database from the Book/models file
query for all the book data in the database
get the total number of books
#Import the Book Database from the Book/model file
In [1]: from Book.models import Book
#Query for all the Book Objects
In [2]: Book.objects.all()
Out[2]: <QuerySet [<Book: Introduction to Django>, <Book: A Practical Guild to Technical Writing>, <Book: Building API with Fast API>, <Book: Training a
Machine Learning Model with Keras>]>
#Get the counts of the book objects
In [3]: Book.objects.all().count()
Out[3]: 4
As seen above, we have loaded the book data with the custom command we created.
Additional Argument
Like most command-line commands, the custom management command we created can accept arguments and perform varying operations based on the arguments we pass in.
The argument could either be optional arguments or positional arguments.
For our command to accept arguments we will have to add an add_arguments
method to our Command class.
I will show you how to add positional arguments and optional arguments in this section.
When our custom command accepts positional arguments, the positional arguments are required and must be provided.
Positional argument
TO-DO
For the positional argument;
we will create a new
delete command
to delete a specific book. The specific book will be identified by itsID
. Therefore, the positional argument our command will accept will be anID
which is an integer.We will query for the Book with the
ID
that we passed to our command.We will delete the book If there is a book with the ID. Else,
we will return an error message that there is no book with such ID.
To create our custom delete command, follow the procedure we followed earlier when we created the populate
command. Do you still remember the procedure?
• Create a delete.py
file in the management/command folder.
• Import BaseCommand and CommandError from django.core.management.base
:
from django.core.management.base import BaseCommand, CommandError
• Import Book model from Book.models:
from Book.models import Book
• Create the required Command class and add the handle
method.
For our custom command to accept Positional arguments, We will add an add_arguments
method to the Command class
, it will take self
and parser
as its arguments. The parser
holds all the necessary information required to parse the command line arguments into Python data types ¶.
The parser argument is an object of the Python Argparse library and the parser has an add_argument
method that allows us to add a single command-line argument and define the behavior of the argument we will add.
Add the method below to the Command class
:
def add_arguments(self, parser):
parser.add_argument("id", nargs=1, type=int, help= “delete a database data with the id that is passed in”)
In the above code snippet, we created the add_arguments
method and we passed self
and parser
to it as an argument.
In the add_arguments
method, the parser
object calls its add_argument
method with the following :
• id
: which is the name of the argument that our command will receive.
• nargs
: which is the number of arguments we can pass to the command.
• type
: which is the data type of the argument that our command will receive.
Aside from these three, the parser.add_argument()
accept several other arguments listed below with their purpose in the official Argparse documentation
TO-DO
In the handle method
of the Command class
we created for our custom delete
command, we will do the following:
we will query for the Book with the ID argument we passed to the
parser.add_argument
.If a book with the ID is present in our database, we will delete it.
If it's not present, we will catch the error in a Try-Except block and print a custom friendly error:
Code Snippet
def handle(self, *args, **options):
book_id = options["id"][0]
try:
book = Book.objects.get(id = book_id)
except:
raise CommandError(f" Book with the id number {book_id} does not exist in the book database")
book.delete()
self.stdout.write(self.style.SUCCESS("Book data deleted successfully !"))
The value of the id that we will pass to our command is stored in the wildcard **options argument
. The **options
is a dictionary of key-value pairs of data.
The id
is a key in the options
method parameter and id
has a value of the list of arguments we passed to our command. That is, if we passed 1 to our command, the value of id
in the **options
method parameter will be [ 1 ], if we passed 1 and 4, the value of id will be [ 1, 4 ].
In our case, we specified the parser.add_argument’s nargs
to 1 — indicating we will pass one argument to our command— hence the id
will be a python list with a list item of the id value that we passed to our command.
To get the value from the list, we need the following line:
book_id = options[“id”][0]
The line gets the first index of the list since the list will have a length of 1.
In the try and except block, we queried the database for the Book object which has an ID of the id
value we will pass to our command :
book = Book.objects.get(id=book_id)
If a book with the id of book_id
does not exist, It will trigger an error telling us the book does not exist:
Book.models.DoesNotExist: Book matching query does not exist.
A wildcard except
( except with no error specified ) will catch this and other errors and raise the CommandError with the error message we specified.
raise CommandError(f" Book with the id number {book_id} does not exist in the book database")
If the book exists, the query will be successful with no error, hence the exception will not be raised.
we then delete the book:
$ book.delete()
If the delete operation was successful ( No error ), We write a success message to the standard output which is the screen:
self.stdout.write(self.style.SUCCESS("Book data deleted successfully !"))
Testing
Just like the populate command, The syntax for our delete command will be:
$ python manage.py delete <id>
With an additional placeholder for the id
value.
To delete a book with the id of 37, type the command below in the terminal:
$ python manage.py delete 37
We get the success message indicating that the command we created deleted the book successfully:
In the python shell environment, let's confirm if the command deletes the book indeed :
$ Book.objects.get(id=35)
So, we got the DoesNotExist
Error with an error message `Book matching the query does not exist since we have already deleted the book. This shows the command work!
Optional Arguments
We must provide the positional argument
that we added to our delete command anytime we use the command.
We can also add an optional command argument
to our delete command which can modify the operation of the delete command. Optional command-line arguments which are also called flag has
-or
--` preceding them.
TODO
I will add an option -i
optional argument to our delete command.
When we pass it to our command in addition to the ID value:
• the delete command
will interact with us and ask if we want to delete the book with the ID value.
• If we type in yes
or y
, the book will be deleted.
• If we type in No
or N
, the command will cancel the operation.
CODE and CODE Description
def add_arguments(self, parser):
parser.add_argument("data", nargs=3)
parser.add_argument("-i", action="store-true",required=False)
We modified the add_arguments method by adding the line below to add a new option to our command:
parser.add_argument(“-i”, action=”store-true”, required=False)
In the line above,
• -i
is the flag name
• action=”store-true”
stores Python Boolean value True
if we pass the optional flag -i
to our command and False
otherwise.
• required=False
means we can choose to leave out the flag -i in our delete command
Modify the handle method as shown below:
def handle(self, *args, **options):
book_id = options["id"][0]
try:
book = Book.objects.get(id = book_id)
except:
raise CommandError(f" Book with the id number {options['id']} does not exist in the book database")
is_i = options["i"]
if is_i:
yes_no = input(f"Are sure you want to delete \'{book}\' book from the book database (y/n): ")
if "y" in yes_no:
book.delete()
else:
self.stdout.write(self.style.SUCCESS("Operation Canceled"))
return
else:
book.delete()
self.stdout.write(self.style.SUCCESS("Book data deleted successfully !"))
In the code above,
We queried for the book and handled the error if the book does not exist.
We get the value of the flag
-i
from theoptions
and assign it to the variableis_i
. The value ofi
can either be True or False. It is True if we pass the ‘-i’ flag to our delete command and false otherwise.If
is_i
is True, we used the Python input function to prompt the user for confirmation before our command deletes the book we specified.If the user input
yes
or a string that containsy
, the command delete the book.Else, if the user inputs any other words, the command cancels the operation.
Testing
Let's test if everything works.
We will try and delete the book with an ID of 38 with an interactive option by adding the -i flag.
In the terminal, enter the delete command with the flag -i
for interaction:
$ python manage.py delete 38 -i
As seen below, the command prompts us for a confirmation:
$ python manage.py delete 38 -i
Are sure you want to delete A Practical Guild to Technical Writing from the book database (y/n): n
When we input n or no, the command cancels the operation.
When we initialized the command again with the -i
flag and input yes when the command prompts us for confirmation, the command deletes the book successfully.
Conclusion
Though Django seems to have built most of what we need to build a functional web application, It also comes with a lot of flexibility. It allows us to create custom things that suit our requirements perfectly. One of the flexibility that Django comes with is adding custom management commands. In this tutorial, I showed you how you can add to the Django manage.py
commands.
We added a populate command that populates the database with data and a delete command to delete a book we specified. We also add an interactive option to the delete command. The interactive option prompts the user for confirmation before performing the delete operation.
Django’s command framework is just an API wrapper on the python Argparse library.
The full code used in this tutorial can be found on Github.