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
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:
FastAPI Application: Initializes a FastAPI instance.
Pydantic Models: Defines
FinanceCreate
for input validation and creating new records andFinanceRead
for output formatting.FinanceRead
includes additional configuration for ORM models.Database Dependency: Provides a
get_db
function to manage database sessions.Create Endpoint: A POST endpoint
/finances/
to add new finance entries to the database.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
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
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
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.