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'}
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.