Performing CRUD operations with a CLI app using Firebase

• 6 min read

For the next phase in this series, we'll learn how to make a CLI app to perform CRUD operations, i.e, Creating, Retrieving, Updating and Deleting data. We'll use Firebase to store our data.

What we'll create

A contacts app with the following functionality:

# add a new contact with name=Peter and mobile=01034512
$ contacts add Peter --mobile 01034512
Contact Peter added!
{ 'mobile': '01034512' }
# view all contacts
$ contacts list
Here's a list of all your contacts:
{ ...  # list of all contacts }
# view single contact
$ contacts view Peter
{ 'mobile': '01034512' }

$ contacts view Meg
The contact you searched for doesn't exist
# update mobile number for Peter
$ contacts update Peter --mobile 00000
Contact updated!
{ 'mobile': '00000' }
# delete the contact Peter
$ contacts delete Peter
Contact deleted!

This post is a bit long. If you just want to take a look at the code or see how the app works, check out the repo.

Setting up Firebase

As mentioned earlier, Firebase will be used to store our data. To set up the database, sign in on Firebase using your google account.

After signing in successfully, click on the Go to console button on the top right. There, you can create a new project. Follow the required steps to create a new project.

Once the project is created, you'll see a Develop sidebar on the left. Click on Database to set up the database. Cloud Firestore will be displayed as the first option but that's not what we'll be using. Scroll down and you'll see a Realtime Database option. Click on Create database. Next, you'll be presented with the security options.

Firebase security rulesoptions

Since we'll not be handling authentication, choose Start in test mode for the security rules. Your database is now created 🎉

Empty firebasedb

Now for the final step in setting up the database, click on the little plus sign + to add a new child node called contacts. This will hold our contacts (obviously 😅). Also, add a test contact just to see how the database will be structured.

Adding a new node to hold ourdata

The database should now look like this:

Finalresult

App setup

We've already gone through the basics of setting up in previous posts so I won't go into much detail on that. Here are the steps:

$ mkdir contacts-cli

$ cd contacts-cli

$ virtualenv env

$ . env/bin/activate

(env) $ mkdir app

(env) $ touch app/__init__.py

(env) $ touch setup.py

(env) $ touch .env

.env

This is where the api endpoint url will be stored. Firebase provides a REST API for the database so that's what we'll use. If you already followed the steps and created the database, just grab the link provided at the top of the database and set it in your .env file.

export URL=https://[YOUR_PROJECT_ID].firebaseio.com

Make sure to run . .env to load your environment variables, otherwise URL will be None.

setup.py

To learn more about the setup file, you can go back to this post.

from setuptools import setup, find_packages

setup(
        name="contacts",
        py_modules=find_packages(),
        install_requires=[
            "Click",
            "requests",
            ],
        entry_points="""
        [console_scripts]
        contacts=app:cli
        """,
        )

Then run pip install --editable . in your terminal to install the app.

  1. app/__init__.py
import click

@click.group(invoke_without_command=True)
@click.pass_context
def cli(ctx):
    if ctx.invoked_subcommand is None:
        click.echo("Welcome to the contacts app 🥳")
        click.echo("Run contacts --help for options.")

We need to use @click.group to allow us to include sub-commands.

If the main command is invoked without any sub-command, a welcome message is shown. How does the main command know if a sub-command has been passed in or not? By looking at the context. if ctx.invoked_subcommand is None, then no sub-command has been passed - show the welcome message.

Everything is setup now, so we can start working on the functionality.

Creating contacts

import os
import requests
import click

...

@cli.command()
@click.argument('name')
@click.option('--mobile', '-m', required=True)
def add(name, mobile):
    """
    Add a new contact
    """
    response = requests.put('{}/contacts/{}.json'
                            .format(os.getenv('URL'), name),
                            json={'mobile': '{}'.format(mobile)})
    click.echo('Contact {} added!'.format(name))
    click.echo(response.json())

You'll notice that I used PUT for the request instead of POST. I did this to avoid generating unique IDs for my data on Firebase. Feel free to use POST instead and see how you can work with the generated UIDs.

The add command takes two parameters - a name argument, and a mobile option. Both are required. Running contacts add --help shows us what options we have.

(env) $ contacts add --help

Usage: contacts add [OPTIONS] NAME

  Add a new contact

Options:
  -m, --mobile TEXT  [required]
  --help             Show this message and exit.

Let's add a new contact:

(env) $ contacts add Chris -m 34243234
Contact Chris added!
{'mobile': '34243234'}

Contactsdatabase

Getting all contacts

...

@cli.command()
def list():
    """
    View all contacts
    """
    response = requests.get('{}/contacts.json'
                            .format(os.getenv('URL')))
    click.echo('Here\'s a list of all your contacts:')
    click.echo(response.json())

The list command simply does a GET request on the contacts node and returns the result.

(env) $ contacts list
Here's a list of all your contacts:
{'Chris': {'mobile': '34243234'}, 'Peter': {'mobile': '8487344'}}

Getting one contact

...

@cli.command()
@click.argument('name')
def view(name):
    """
    View single contact
    """
    response = requests.get('{}/contacts/{}.json'
                            .format(os.getenv('URL'), name))
    if not response.json():
        click.echo("The contact you searched for does'nt exist")
    else:
        click.echo(response.json())

The view command is similar to list, except it takes a name argument and adds it to the url to return the contact for that name only. If the name doesn't exist in the database, null is returned. We take advantage of that in the if block to return the appropriate response.

(env) $ contacts view Peter
{'mobile': '8487344'}

(env) $ contacts view Meg
The contact you searched for doesn't exist

Updating a contact

...

@cli.command()
@click.argument('name')
@click.option('--mobile', '-m', required=True)
def update(name, mobile):
    """
    Update contact
    """
    response = requests.patch('{}/contacts/{}.json'
                              .format(os.getenv('URL'), name),
                              json={'mobile': '{}'.format(mobile)})
    click.echo('Contact updated!')
    click.echo(response.json())

This works exactly the same to the create command, except we're using PATCH instead of PUT (which apparently, doesn't make any difference here. Making a PATCH on a non-existent node creates it, just like PUT would do. Try it out.)

(env) $ contacts update Peter -m 552
Contact updated!
{'mobile': '552'}

And finally, deleting a contact

Similar to the view command. The only change we'll make is using DELETE instead of GET.

...

@cli.command()
@click.argument('name')
def delete(name):
    """
    Delete contact
    """
    requests.delete('{}/contacts/{}.json'
                    .format(os.getenv('URL'), name))
    click.echo('Contact deleted!')

null is returned on successful deletion so we don't require to return response. The request is called directly, and a success message is shown afterwords. This can be done better but it'll do for now.

(env) $ contacts delete Peter
Contact deleted!

I meant to cover exporting (writing) the data to a local JSON file or sqlite database, but this post is already too long. These are interesting things to try if you want to learn more.

In the next post, we'll add tests to the app using pytest.