Building a REST API Client in Python with PyQt5

 

In this tutorial, we’ll explore how to build a Python-based GUI client using PyQt5 to interact with a REST API. We’ll cover the complete implementation of basic CRUD operations and explain key considerations for RESTful API interaction, including the use of headers like Cache-Control.

Prerequisites

Before diving into the PyQt5 client, you’ll want to ensure you’ve followed the setup in the previous article on building a REST API with native PHP (link to prior blog), where we set up endpoints to handle product and category data. Additionally, we created a C# client for the same API, which you can review here from the previous article Building a REST API Client in C# with Windows Form to gain insights into other approaches for interacting with REST APIs.

Overview of the Project

This client will allow us to:

  • Retrieve and display product lists from the server
  • Add new products
  • Update existing product information
  • Delete products

Our PyQt5 client will communicate with our REST API server using Python’s requests library, and it will leverage Qt widgets for user interface components.

Setting Up the API Client

Step 1: Install Dependencies

If you haven’t already, install PyQt5 and the requests library:

pip install PyQt5 requests

Step 2: Code for the PyQt5 API Client

Below is a simplified structure for the PyQt5 client. Here’s how it’s set up:

  • Retrieve Products: Get a list of all products from the API.
  • Create Product: Add a new product.
  • Update Product: Modify an existing product.
  • Delete Product: Remove a product from the list.

Complete Source Code Implementation

import sys

import requests
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLineEdit, QMessageBox, \
    QFormLayout, QTableWidget, QTableWidgetItem, QHeaderView

# API_BASE_URL = 'http://localhost:5000'  # Replace with your API URL
API_BASE_URL = 'https://bcssti.com/api-sample'
HEADERS = {
    'Cache-Control': 'no-cache',
    'Content-type': 'application/json',
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'
}


class ApiClientApp(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Product API Client")
        self.setGeometry(100, 100, 600, 400)

        self.layout = QVBoxLayout()

        # Form layout for input fields
        self.formLayout = QFormLayout()
        self.productIdInput = QLineEdit()
        self.productNameInput = QLineEdit()
        self.descriptionInput = QLineEdit()
        self.productPriceInput = QLineEdit()

        self.formLayout.addRow("ID:", self.productIdInput)
        self.formLayout.addRow("Name:", self.productNameInput)
        self.formLayout.addRow("Description:", self.descriptionInput)
        self.formLayout.addRow("Price:", self.productPriceInput)

        self.layout.addLayout(self.formLayout)

        # Buttons
        self.getAllButton = QPushButton("Get All Products")
        self.getButton = QPushButton("Get Product")
        self.createButton = QPushButton("Create Product")
        self.updateButton = QPushButton("Update Product")
        self.deleteButton = QPushButton("Delete Product")

        self.layout.addWidget(self.getAllButton)
        self.layout.addWidget(self.getButton)
        self.layout.addWidget(self.createButton)
        self.layout.addWidget(self.updateButton)
        self.layout.addWidget(self.deleteButton)

        # Table to display products
        self.productTable = QTableWidget()
        self.productTable.setColumnCount(3)
        self.productTable.setHorizontalHeaderLabels(["ID", "Name", "Description", "Price"])
        self.productTable.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)

        self.layout.addWidget(self.productTable)

        # Connect buttons to functions
        self.getAllButton.clicked.connect(self.get_all_products)
        self.getButton.clicked.connect(self.get_product)
        self.createButton.clicked.connect(self.create_product)
        self.updateButton.clicked.connect(self.update_product)
        self.deleteButton.clicked.connect(self.delete_product)

        self.setLayout(self.layout)

    def get_all_products(self):
        # try:
        response = requests.get(f"{API_BASE_URL}/products/", headers=HEADERS)
        response.raise_for_status()
        products = response.json()
        self.show_products_in_table(products)
        # except requests.exceptions.RequestException as e:
        #    QMessageBox.critical(self, "Error", f"Failed to fetch products: {e}")

    def get_product(self):
        product_id = self.productIdInput.text()
        if not product_id:
            QMessageBox.warning(self, "Warning", "Please enter a Product ID.")
            return

        try:
            response = requests.get(f"{API_BASE_URL}/products?id={product_id}", headers=HEADERS)
            response.raise_for_status()
            product = response.json()
            self.productNameInput.setText(product.get("name", ""))
            self.descriptionInput.setText(product.get("description", ""))
            self.productPriceInput.setText(str(product.get("price", "")))
            self.show_products_in_table([product])
        except requests.exceptions.RequestException as e:
            QMessageBox.critical(self, "Error", f"Failed to fetch product: {e}")

    def create_product(self):
        name = self.productNameInput.text()
        description = self.descriptionInput.text()
        price = self.productPriceInput.text()

        if not name or not price:
            QMessageBox.warning(self, "Warning", "Please enter product name and price.")
            return

        try:
            data = {"name": name, "description": description, "price": float(price)}
            response = requests.post(f"{API_BASE_URL}/products", json=data, headers=HEADERS)
            response.raise_for_status()
            if response.status_code == 200:
                QMessageBox.information(self, "Success", response.json().get("message", "Product created"))
                self.get_all_products()  # Refresh after update
            else:
                QMessageBox.warning(self, "Warning", "Failed to add new  product.")
            self.get_all_products()
        except requests.exceptions.RequestException as e:
            QMessageBox.critical(self, "Error", f"Failed to create product: {e}")


def update_product(self):
    product_id = self.productIdInput.text()
    name = self.productNameInput.text()
    description = self.descriptionInput.text()
    price = self.productPriceInput.text()

    if not product_id or not name or not price:
        QMessageBox.warning(self, "Warning", "Please enter Product ID, name, and price.")
        return

    try:
        data = {"name": name, "description": description, "price": float(price)}
        response = requests.put(f"{API_BASE_URL}/products?id={product_id}", json=data, headers=HEADERS)
        response.raise_for_status()
        if response.status_code == 200:
            QMessageBox.information(self, "Success", response.json().get("message", "Product updated"))
            self.get_all_products()  # Refresh after update
        else:
            QMessageBox.warning(self, "Warning", "Failed to update product.")
    except requests.exceptions.RequestException as e:
        QMessageBox.critical(self, "Error", f"Failed to update product: {e}")


def delete_product(self):
    product_id = self.productIdInput.text()
    if not product_id:
        QMessageBox.warning(self, "Warning", "Please enter a Product ID.")
        return

    try:
        response = requests.delete(f"{API_BASE_URL}/products?id={product_id}", headers=HEADERS)
        response.raise_for_status()
        QMessageBox.information(self, "Success", response.json().get("message", "Product deleted"))
        self.get_all_products()
    except requests.exceptions.RequestException as e:
        QMessageBox.critical(self, "Error", f"Failed to delete product: {e}")


def show_products_in_table(self, products):
    self.productTable.setRowCount(len(products))
    for row, product in enumerate(products):
        self.productTable.setItem(row, 0, QTableWidgetItem(str(product.get("id", ""))))
        self.productTable.setItem(row, 1, QTableWidgetItem(product.get("name", "")))
        self.productTable.setItem(row, 2, QTableWidgetItem(product.get("description", "")))
        self.productTable.setItem(row, 3, QTableWidgetItem(str(product.get("price", ""))))


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = ApiClientApp()
    window.show()
    sys.exit(app.exec_())

Key Points

1. Adding Headers

headers = {'Cache-Control': 'no-cache'}

The Cache-Control: no-cache header is important in REST API requests when working with frequently updated data, like our product data. It ensures that each request retrieves the latest data from the server rather than using potentially outdated cached responses.

2. Error Handling and Input Validation

  • Input Validation: We check if the name and price fields are populated before sending a request. This avoids sending incomplete data and improves user experience.
  • Error Handling: Wrapping the network request in a try-except block allows us to catch exceptions if the server is unreachable or if there’s an unexpected error.

3. Why Use Cache-Control: no-cache?

In scenarios where data consistency is critical

—such as product listings that might change frequently—it’s essential to ensure we aren’t accidentally viewing stale data. Cache-Control: no-cache instructs the client to validate data with the server always, ensuring that any changes on the server are immediately reflected in the client application.


Next Steps

Now that we’ve created the Python client, you can further expand it by:

  • Adding new API endpoints for handling categories or additional product attributes.
  • Implementing a refresh button to update the list of products without restarting the application.

You can also experiment with the downloadable POSTMAN collection from the previous articles, which provides a hands-on way to test API interactions.


Previous Post Next Post

نموذج الاتصال