Finance Manager Tutorial - Part 3

In this part of the tutorial, we will extend our finance manager app to create a REST API using FastAPI and Uvicorn. This will allow us to perform server-side operations and interact with the SQLite database created in Part 2 using [SQLAlchemy].

FastAPI is a modern, fast (high-performance), web framework for building APIs with Python. It is built on top of [ASGI (Asynchronous Server Gateway Interface)], which allows for efficient handling of concurrent requests.

Uvicorn is a lightning-fast ASGI server implementation, using uvloop and httptools. It provides a high-performance server for running FastAPI applications.

To download the complete source code for this tutorial, visit Finance Manager Example - Part 3.

Prerequisites

Before we begin, make sure you have FastAPI and Uvicorn installed within your Python environment.

You can install them using pip:

pip install fastapi uvicorn

Project Structure

The overall project structure in this part of the tutorial is much different from the previous parts. We move the frontend part that uses PySide6 and QML to a separate directory called Frontend and the backend part that uses FastAPI to create a REST API to the SQLite database to a directory called Backend.

├── Backend
│   ├── database.py
│   ├── main.py
│   └── rest_api.py
├── Frontend
│   ├── Finance
│   │   ├── AddDialog.qml
│   │   ├── FinanceDelegate.qml
│   │   ├── FinancePieChart.qml
│   │   ├── FinanceView.qml
│   │   ├── Main.qml
│   │   └── qmldir
│   ├── financemodel.py
│   └── main.py

Let’s Get Started!

First, let’s create the Backend directory and move the database.py from the previous part to the Backend directory.

Backend Setup

Setting Up FastAPI

Create a new Python file rest_api.py in the Backend directory and add the following code:

rest_api.py
rest_api.py
 1# Copyright (C) 2024 The Qt Company Ltd.
 2# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
 3
 4import logging
 5from fastapi import FastAPI, Depends, HTTPException
 6from pydantic import BaseModel
 7from typing import Dict, Any
 8from sqlalchemy import orm
 9from database import Session, Finance
10
11app = FastAPI()
12
13
14class FinanceCreate(BaseModel):
15    item_name: str
16    category: str
17    cost: float
18    date: str
19
20
21class FinanceRead(FinanceCreate):
22    class Config:
23        from_attributes = True
24
25
26def get_db():
27    db = Session()
28    try:
29        yield db
30    finally:
31        db.close()
32
33
34@app.post("/finances/", response_model=FinanceRead)
35def create_finance(finance: FinanceCreate, db: orm.Session = Depends(get_db)):
36    print(f"Adding finance item: {finance}")
37    db_finance = Finance(**finance.model_dump())
38    db.add(db_finance)
39    db.commit()
40    db.refresh(db_finance)
41    return db_finance
42
43
44@app.get("/finances/", response_model=Dict[str, Any])
45def read_finances(skip: int = 0, limit: int = 10, db: orm.Session = Depends(get_db)):
46    try:
47        total = db.query(Finance).count()
48        finances = db.query(Finance).offset(skip).limit(limit).all()
49        response = {
50            "total": total,
51            # Convert the list of Finance objects to a list of FinanceRead objects
52            "items": [FinanceRead.from_orm(finance) for finance in finances]
53        }
54        logging.info(f"Response: {response}")
55        return response
56    except Exception as e:
57        logging.error(f"Error occurred: {e}")
58        raise HTTPException(status_code=500, detail="Internal Server Error")

In rest_api.py, we set up a FastAPI application to handle Create and Read operations for the finance data. The file includes:

  1. FastAPI Application: Initializes a FastAPI instance.

  2. Pydantic Models: Defines FinanceCreate for input validation and creating new records and FinanceRead for output formatting. FinanceRead includes additional configuration for ORM models.

  3. Database Dependency: Provides a get_db function to manage database sessions.

  4. Create Endpoint: A POST endpoint /finances/ to add new finance entries to the database.

  5. Read Endpoint: A GET endpoint /finances/ to retrieve finance entries from the database.

This setup allows the application to interact with the database, enabling the creation and retrieval of finance records via RESTful API endpoints.

Creating a main Python File for the Backend

Create a new Python file main.py in the Backend directory and add the following code:

main.py
main.py
 1# Copyright (C) 2024 The Qt Company Ltd.
 2# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
 3
 4import uvicorn
 5from database import initialize_database
 6
 7
 8def main():
 9    # Initialize the database
10    initialize_database()
11    # Start the FastAPI endpoint
12    uvicorn.run("rest_api:app", host="127.0.0.1", port=8000, reload=True)
13
14
15if __name__ == "__main__":
16    main()

In main.py, along with initializing the database we create a Uvicorn server instance to run the FastAPI application. The server runs on 127.0.0.1 at port 8000 and reloads automatically when code changes are detected.

Frontend Setup

Then we move on to the frontend part of the application, and connect it to the FastAPI backend. We will create a new directory Frontend. Move the folder Finance and the files financemodel.py and main.py from the previous parts to the Frontend directory.

Most of the code remains the same as in the previous parts, with minor changes to connect to the REST API.

Updating the FinanceModel Class

The following changes (highlighted) are made to the financemodel.py file which removes the database interaction code from Python and adds the REST API interaction code.

financemodel.py
financemodel.py
  1# Copyright (C) 2024 The Qt Company Ltd.
  2# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
  3
  4import requests
  5from datetime import datetime
  6from dataclasses import dataclass
  7from enum import IntEnum
  8from collections import defaultdict
  9
 10from PySide6.QtCore import (QAbstractListModel, QEnum, Qt, QModelIndex, Slot,
 11                            QByteArray)
 12from PySide6.QtQml import QmlElement
 13
 14QML_IMPORT_NAME = "Finance"
 15QML_IMPORT_MAJOR_VERSION = 1
 16
 17
 18@QmlElement
 19class FinanceModel(QAbstractListModel):
 20
 21    @QEnum
 22    class FinanceRole(IntEnum):
 23        ItemNameRole = Qt.DisplayRole
 24        CategoryRole = Qt.UserRole
 25        CostRole = Qt.UserRole + 1
 26        DateRole = Qt.UserRole + 2
 27        MonthRole = Qt.UserRole + 3
 28
 29    @dataclass
 30    class Finance:
 31        item_name: str
 32        category: str
 33        cost: float
 34        date: str
 35
 36        @property
 37        def month(self):
 38            return datetime.strptime(self.date, "%d-%m-%Y").strftime("%B %Y")
 39
 40    def __init__(self, parent=None) -> None:
 41        super().__init__(parent)
 42        self.m_finances = []
 43        self.fetchAllData()
 44
 45    def fetchAllData(self):
 46        response = requests.get("http://127.0.0.1:8000/finances/")
 47        try:
 48            data = response.json()
 49        except requests.exceptions.JSONDecodeError:
 50            print("Failed to decode JSON response")
 51            return
 52        self.beginInsertRows(QModelIndex(), 0, len(data["items"]) - 1)
 53        self.m_finances.extend([self.Finance(**item) for item in data["items"]])
 54        self.endInsertRows()
 55
 56    def rowCount(self, parent=QModelIndex()):
 57        return len(self.m_finances)
 58
 59    def data(self, index: QModelIndex, role: int):
 60        if not index.isValid() or index.row() >= self.rowCount():
 61            return None
 62        row = index.row()
 63        if row < self.rowCount():
 64            finance = self.m_finances[row]
 65            if role == FinanceModel.FinanceRole.ItemNameRole:
 66                return finance.item_name
 67            if role == FinanceModel.FinanceRole.CategoryRole:
 68                return finance.category
 69            if role == FinanceModel.FinanceRole.CostRole:
 70                return finance.cost
 71            if role == FinanceModel.FinanceRole.DateRole:
 72                return finance.date
 73            if role == FinanceModel.FinanceRole.MonthRole:
 74                return finance.month
 75        return None
 76
 77    def roleNames(self):
 78        roles = super().roleNames()
 79        roles[FinanceModel.FinanceRole.ItemNameRole] = QByteArray(b"item_name")
 80        roles[FinanceModel.FinanceRole.CategoryRole] = QByteArray(b"category")
 81        roles[FinanceModel.FinanceRole.CostRole] = QByteArray(b"cost")
 82        roles[FinanceModel.FinanceRole.DateRole] = QByteArray(b"date")
 83        roles[FinanceModel.FinanceRole.MonthRole] = QByteArray(b"month")
 84        return roles
 85
 86    @Slot(int, result='QVariantMap')
 87    def get(self, row: int):
 88        finance = self.m_finances[row]
 89        return {"item_name": finance.item_name, "category": finance.category,
 90                "cost": finance.cost, "date": finance.date}
 91
 92    @Slot(str, str, float, str)
 93    def append(self, item_name: str, category: str, cost: float, date: str):
 94        finance = {"item_name": item_name, "category": category, "cost": cost, "date": date}
 95        response = requests.post("http://127.0.0.1:8000/finances/", json=finance)
 96        if response.status_code == 200:
 97            finance = response.json()
 98            self.beginInsertRows(QModelIndex(), 0, 0)
 99            self.m_finances.insert(0, self.Finance(**finance))
100            self.endInsertRows()
101        else:
102            print("Failed to add finance item")
103
104    @Slot(result=dict)
105    def getCategoryData(self):
106        category_data = defaultdict(float)
107        for finance in self.m_finances:
108            category_data[finance.category] += finance.cost
109        return dict(category_data)

Two methods are overridden for the FinanceModel class - fetchMore and canFetchMore. These methods are used to fetch more data from the REST API when the model is scrolled to the end. The data is fetched in chunks of 10 entries at a time. Additionally, the append method is updated to send a POST request to the REST API to add a new finance entry.

Updating the Main Python File for the Frontend

Finally, we update the main.py file in the Frontend directory to remove the database initialization code.

main.py
main.py
 1# Copyright (C) 2024 The Qt Company Ltd.
 2# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
 3
 4import sys
 5from pathlib import Path
 6
 7from PySide6.QtWidgets import QApplication
 8from PySide6.QtQml import QQmlApplicationEngine
 9
10from financemodel import FinanceModel  # noqa: F401
11
12if __name__ == '__main__':
13    app = QApplication(sys.argv)
14    QApplication.setOrganizationName("QtProject")
15    QApplication.setApplicationName("Finance Manager")
16    engine = QQmlApplicationEngine()
17
18    engine.addImportPath(Path(__file__).parent)
19    engine.loadFromModule("Finance", "Main")
20
21    if not engine.rootObjects():
22        sys.exit(-1)
23
24    exit_code = app.exec()
25    del engine
26    sys.exit(exit_code)

The rest of the code and the QML files remains the same as in the previous parts of the tutorial.

Running the Application

To run the application, first start the backend FastAPI server by executing the main.py file in the Backend directory using Python:

python Backend/main.py

This will start the Uvicorn server running the FastAPI application on http://127.0.0.1:8000.

After starting the backend server, run the frontend application by executing the main.py file in the Frontend directory using Python:

python Frontend/main.py

This will start the PySide6 application that connects to the FastAPI backend to display the finance data.

Deployment

Deploying the Application

To deploy the application, follow the same steps as in the first part of the tutorial.

Summary

At the end of this tutorial, you have extended the finance manager app to include a REST API using FastAPI and Uvicorn. The backend application interacts with the SQLite database using SQLAlchemy and provides endpoints to create and retrieve finance records. The frontend application connects to the backend using the REST API to display the finance data.

If you want to extend the application further, you can add more features like updating and deleting finance records, adding user authentication etc.