In the previous post, we saw how to get started with Typer including commands, arguments and options. We also saw how to define styles using Rich to format text. This post will show how to get started with SQLModel to store and retrieve data in a SQLite database.

Defining the Data Model Class

Before using SQLModel it needs to be installed. As it is distributed as a Python package, you can use pip to install it.

$ pip install sqlmodel

SQLModel leverages SQLAlchemy. If you have used SQLAlchemy you’ll see that the SQLModel API mirrors the SQLAlchemy API in many cases. The data model class will be associated with a table in the SQLite database. It will subclass the SQLModel base class which needs to be imported.

from sqlmodel import SQLModel

Now create a new class, Event, that subclasses SQLModel. Also, tell SQLModel this will be associated with a database table by setting the table keyword argument to True.

class Event(SQLModel, table=True):

The columns of the database table will be associated with attributes of the model class. These can be defined similar to how you would define attributes of a dataclass. For example, the title of the event is a simple string.

class Event(SQLModel, table=True):
  title: str

There is also a boolean marking this event as private if set to True. However, we can also set a default value and make events publicly visible by default.

class Event(SQLModel, table=True):
  title: str
  private: bool = False

Sometimes you need more than just a simple type. The Field type configures a column/attribute association with extra metadata. The Field type needs to be imported from the sqlmodel module:

from sqlmodel import Field, SQLModel

Every table needs a primary key that is an integer. But we also need to tell SQLModel that this attribute is the primary key of the model class. Also, set the default keyword argument to None so that we can get away with waiting for SQLModel to add the value later.

class Event(SQLModel, table=True):
  id: int = Field(primary_key=True, default=None)
  title: str
  private: bool = False

The model class also needs a timestamp. This will be a Python date from the datetime module. But we can also use the default_factory keyword argument to use the today function of the datetime.date type to use the current date as the default value.

import datetime

class Event(SQLModel, table=True):
  id: int = Field(primary_key=True, default=None)
  title: str
  timestamp: datetime.date = Field(default_factory=datetime.date.today)

With the data model class, we can now use SQLModel to generate the database. Import the create_engine function from the sqlmodel module:

from sqlmodel import Field, SQLModel, create_engine

An engine is a connection to a database, in this case a SQLite database file. The create_engine function accepts a path to that database file.

engine = create_engine("sqlite:///events.db")

And you can create the database by calling the create_all function on the metadata of SQLModel. Pass create_all the engine so it will know which database to create.

SQLModel.metadata.create_all(engine)

Getting User Input

The next step is to create a new command that will prompt the user for details about an event and use that input to add a new Event to the database. While Typer has an API to prompt the user for input, since we are using Rich, I am going to use the Prompt type in the rich.prompt module.

from rich.prompt import Prompt

Call the ask function on the Prompt type to get input from the user returned as a string. The Event model class expects more diverse data types so we will need to parse the timestamp string and check if a private event has been confirmed too.

@app.command()
def add_event():
  title = Prompt.ask("Title of the event")
  timestamp = datetime.datetime.strptime(
    Prompt.ask("Date of event (YYYY-MM-DD)"),
    "%Y-%m-%d"
  ).date()
  private = Prompt.ask("Private event (y/n)").lower() == "y"

To interact with the database, you’ll use a Session. Import it from the sqlmodel module.

from sqlmodel import Field, Session, SQLModel, create_engine

Use the engine to create a Session. This is often done in a with block to ensure that the Session is automatically closed.

with Session(engine) as session:

Now create and populate an instance of the Event model class, add it to the Session and commit the changes to the SQLite database.

with Session(engine) as session:
  event = Event(title=title, timestamp=timestamp, private=private)
  session.add(event)
  session.commit()

Now we need to display a confirmation message to the user. For now, we’ll assume the user won’t make a mistake. A bold green style would work for the message and it’s likely we will want to use this more than once. Add a new success style to the ApplicationStyles dataclass.

@dataclass 
class ApplicationStyles:
  error: Style = Style(color="red", bold=True)
  succcess: Style = Style(color="green", bold=True)
  
def add_event():
  # ...
  console.print(
    f"Added event with title {event.title}", 
    style=app_styles.success,
  )

Now you can try out the new command! Since the command function is add_event, the command name will be almost the same, add-event the only difference is the the underscore in the function name is a dash in the command.

$ python main.py add-event
Title of the event: New Event
Date of event (YYYY-MM-DD): 2025-02-02
Private event (y/n): n

And the result:

If you are using Visual Studio Code, you can install the SQLite Viewer extension by Florian Klampfer to peek inside the database and ensure the event was added.

In the Explorer pane, right click on the database and Open With. In the Command Pallette, select SQLite Viewer.

Displaying Data from the Database

Let’s look at how to retrieve the Event we just added to the database. Create a new command with a function called show_events():

@app.command()
def show_events():

You’ll need a Session object to interact with the database:

  with Session(engine) as session:

Now you’re going to construct a query using functions from the SQLModel API. This API often mimics the SQLAlchemy API. In this case you’ll need the select function from the sqlmodel package. Construct a query to select all Events. Then call the exec method on the session to execute the query. And to retrieve all of the results, call the all() method.

from sqlmodel import select

# ...

    events = session.exec(select(Event)).all()

You can iterate over the events like a list.

    for event in events:
        console.print("{event.title}")

Here is the result of invoking the show-events command:

$ python main.py show-events
Event One
Event Two
Event Three

This works, but we can make it look better with the help of a Rich layout. Import the Table from the rich.table module:

from rich.table import Table

A table can have a title and one or more columns. Let’s include columns for title, days remaining, and if the event is private.

table = Table(title="Events")
table.add_column("Title")
table.add_column("Days Remaining")
table.add_column("Private")

Iterate over the events and calculate the days remaining. Then add a row for each event.

for event in events:
  days_remaining = (event.timestamp - datetime.date.today()).days
  table.add_row(event.title, f"{days_remaining} remaining", "Yes" if event.private else "No")

And use the console to print the table:

  console.print(table)

Invoke the show-events command and the results will look something like this:

$ python main.py show-events
                  Events                  
┏━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━┓
 Title        Days Remaining  Private 
┡━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━┩
 Event One    24 remaining    No      
 Event Two    53 remaining    Yes     
 Event Three  85 remaining    No      
└─────────────┴────────────────┴─────────┘

In a future post we will see how to use styles to emphasize datapoints about the data.

Summary

In this post you learned how to implement a data model using SQLModel. You also learned how to use the SQLModel API to connect to a database and store and retrive data from the database. You also learned how to use Rich tables to display data in tabular layout.

By Douglas Starnes

Entrepreneur, 5x Microsoft MVP, AI/BI nerd, crypto investor, content creator, trained composer, challenging the status quo, proud American

Leave a Reply

Your email address will not be published. Required fields are marked *