How to build a Contact Book Application in Python using Rich, Typer and TinyDB
Introduction
In this Python tutorial, we'll learn how to build a terminal application (CLI app) to manage our contact book. We'll use Typer for building the CLI app, Rich for a colorized terminal output, and TinyDB for the database.
Get your tools ready
We'll be using a few external libraries in this project. Let's learn more about them and install them one by one.
But before we install them, let's create a virtual environment and activate it.
We are going to create a virtual environment using virtualenv
. Python now ships with a pre-installed virtualenv
library. So, to create a virtual environment, you can use the below command:
$ python -m venv env
The above command will create a virtual environment named env
. Now, we need to activate the environment using the command:
$ . env/Scripts/activate
To verify if the environment has been activated or not, you can see (env)
in your terminal. Now, we can install the libraries.
Rich: Rich is a Python library for writing rich text (with color and style) to the terminal, and for displaying advanced content such as tables, markdown, and syntax highlighted code.
To install Rich, use the command:
$ pip install Rich
Typer: Typer is a library for building CLI applications.
To install Typer, use the command:
$ pip install Typer
TinyDB: TinyDB is a document-oriented database written in pure Python with no external dependencies.
To install TinyDB, use the command:
$ pip install TinyDB
Features of Contact Book
Our Contact Book application will be a terminal-based application. Similar to a Todo application, we can perform the following operations on it:
- Add (or Create): You can add a new contact in the contact book.
- Show (or Read): You can see all your contacts saved in the contact book.
- Edit (or Update): You can edit the contacts saved in the contact book.
- Remove (or Delete): You can delete the contacts saved in the contact book.
Create a Contact Model
First of all, we'll create a custom class or a model for our Contact. Think of all the fields that contact should have. I can think of these fields - name and contact number. If you can think of more, you can add them to your model. We'll be moving forward with these two for now.
Create a directory called contact_book
and inside that, create a Python file called model.py
. Then add the following content in the file:
import datetime
class Contact:
def __init__ (self, name, contact_number, position=None, date_created=None, date_updated=None):
self.name = name
self.contact_number = contact_number
self.position = position
self.date_created = date_created if date_created is not None else datetime.datetime.now().isoformat()
self.date_updated = date_updated if date_updated is not None else datetime.datetime.now().isoformat()
def __repr__ (self) -> str:
return f"({self.name}, {self.contact_number}, {self.position}, {self.date_created}, {self.date_updated})"
We have created a class called Contact which takes two mandatory parameters name
and contact_number
. Apart from these two, it also takes three optional parameters - position
, date_created
and date_updated
. If these three optional parameters are not passed, they default to the current index and the current times respectively. Further, we have defined the __repr__
method that returns the object in a more readable way.
Create a database using TinyDB
Now, let us set up TinyDB and create a database. If you're new to TinyDB, make sure you check out this tutorial. Inside the contact_book
directory, create a __init__.py
file and add the following content there.
from tinydb import TinyDB, Query
db = TinyDB('contact-book.json')
db.default_table_name = 'contact-book'
ContactQuery = Query()
We have created an instance of the TinyDB class and passed the filename to it. This will create a JSON file contact-book.json
where our data will be stored. To retrieve data from this database, we'll require an instance of the Query class from the tinydb
library.
Now, let us define the different functions which we'll be using to interact with the database. Inside the contact_book
directory, create a database.py
file and add the following content there.
from typing import List
import datetime
from contact_book.model import Contact
from contact_book import db, ContactQuery
def create(contact: Contact) -> None:
contact.position = len(db)+1
new_contact = {
'name': contact.name,
'contact_number': contact.contact_number,
'position': contact.position,
'date_created': contact.date_created,
'date_updated': contact.date_updated
}
db.insert(new_contact)
def read() -> List[Contact]:
results = db.all()
contacts = []
for result in results:
new_contact = Contact(result['name'], result['contact_number'], result['position'],
result['date_created'], result['date_updated'])
contacts.append(new_contact)
return contacts
def update(position: int, name: str, contact_number: str) -> None:
if name is not None and contact_number is not None:
db.update({'name': name, 'contact_number': contact_number},
ContactQuery.position == position)
elif name is not None:
db.update({'name': name}, ContactQuery.position == position)
elif contact_number is not None:
db.update({'contact_number': contact_number},
ContactQuery.position == position)
def delete(position) -> None:
count = len(db)
db.remove(ContactQuery.position == position)
for pos in range(position+1, count):
change_position(pos, pos-1)
def change_position(old_position: int, new_position: int) -> None:
db.update({'position': new_position},
ContactQuery.position == old_position)
We have defined four different functions - create()
, read()
, update()
and delete()
for each of the operations mentioned above. We are using the position
attribute to identify particular contact. The change_position()
function is responsible to maintain the position of the contact whenever a contact is deleted.
Create a CLI using Typer
Now let us create our CLI using Typer. Outside the contact_book
directory, create a main.py
file and add the following content.
import typer
app = typer.Typer()
@app.command(short_help='adds a contact')
def add(name: str, contact_number: str):
typer.echo(f"Adding {name}, {contact_number}")
@app.command(short_help='shows all contacts')
def show():
typer.echo(f"All Contacts")
@app.command(short_help='edits a contact')
def edit(position: int, name: str = None, contact_number: str = None):
typer.echo(f"Editing {position}")
@app.command(short_help='removes a contact')
def remove(position: int):
typer.echo(f"Removing {position}")
if __name__ == " __main__":
app()
First of all, we create an instance of the Typer class from the typer
library. Then we create four separate functions for the four operations that we discussed above. We bind each of the functions with a command using the @app.command()
decorator. We also add short_help
just to help the user with the commands.
To add a contact, we require the name
and contact_number
parameters. To show the contacts, we need nothing. To edit the contact, we necessarily need the position
whereas the name
and contact_number
parameters are optional. To remove the contact, we just need the position
.
For now, we are not doing any operation inside the methods. We are just printing using the echo
method in the typer
class. In the main method, we just need to call the app()
object.
If you run the application, you will get a similar output:
Style the terminal using Rich
We want to display the contacts in a beautiful-looking table layout with different colors. Rich can help us with this. If you're new to Rich, make sure you check out this tutorial.
Now let's modify the show()
function in main.py
as it is responsible to print the contacts on the terminal.
from rich.console import Console
from rich.table import Table
console = Console()
@app.command(short_help='shows all contacts')
def show():
contacts = [("Ashutosh Krishna", "+91 1234554321"),
("Bobby Kumar", "+91 9876556789")]
console.print("[bold magenta]Contact Book[/bold magenta]", "๐")
if len(contacts) == 0:
console.print("[bold red]No contacts to show[/bold red]")
else:
table = Table(show_header=True, header_style="bold blue", show_lines=True)
table.add_column("#", style="dim", width=3, justify="center")
table.add_column("Name", min_width=20, justify="center")
table.add_column("Contact Number", min_width=12, justify="center")
for idx, contact in enumerate(contacts, start=1):
table.add_row(str(idx), f'[cyan]{contact[0]}[/cyan]', f'[green]{contact[1]}[/green]')
console.print(table)
We have first created an instance of the Console class. Inside the show()
method, we have a dummy list of contacts for now. Using the console
object, we print the heading in a bold magenta color. Next, we create a table and add the columns. Now we iterate over the contacts and put them in the table as separate rows with different colors. Then at the end, we print the table.
Connect database operations with Typer commands
Now let us do the final step which is connecting the database operations with the commands, i,e., when we run a command, it should interact with the database appropriately.
import typer
from rich.console import Console
from rich.table import Table
from contact_book.model import Contact
from contact_book.database import create, read, update, delete
app = typer.Typer()
console = Console()
@app.command(short_help='adds a contact')
def add(name: str, contact_number: str):
typer.echo(f"Adding {name}, {contact_number}")
contact = Contact(name, contact_number)
create(contact)
show()
@app.command(short_help='shows all contacts')
def show():
contacts = read()
console.print("[bold magenta]Contact Book[/bold magenta]", "๐")
if len(contacts) == 0:
console.print("[bold red]No contacts to show[/bold red]")
else:
table = Table(show_header=True,
header_style="bold blue", show_lines=True)
table.add_column("#", style="dim", width=3, justify="center")
table.add_column("Name", min_width=20, justify="center")
table.add_column("Contact Number", min_width=12, justify="center")
for idx, contact in enumerate(contacts, start=1):
table.add_row(str(
idx), f'[cyan]{contact.name}[/cyan]', f'[green]{contact.contact_number}[/green]')
console.print(table)
@app.command(short_help='edits a contact')
def edit(position: int, name: str = None, contact_number: str = None):
typer.echo(f"Editing {position}")
update(position, name, contact_number)
show()
@app.command(short_help='removes a contact')
def remove(position: int):
typer.echo(f"Removing {position}")
delete(position)
show()
if __name__ == " __main__":
app()
In the above code, we have used the create()
, read()
, update()
and delete()
created previously.
Demo
Here's the demo of the final application:
Conclusion
Congratulations! Now you should have a fully functioning CLI app. Now go ahead and try different commands to modify your own contact book!
The code is also available on GitHub.
If you enjoyed this tutorial, please share it with your friends!