analitics

Pages

Showing posts with label PyQt6. Show all posts
Showing posts with label PyQt6. Show all posts

Wednesday, March 6, 2024

Python Qt6 : Test application with unittest and QtTest.

In this simple example, I add a test class with unittest to sleep the application and QtTest to wait for the window to open after pressing the button.
To run the unittest you need to uncomment this row of source code:
#unittest.main()
Let's see the source code:
import sys

from PyQt6.QtWidgets import QApplication, QDialog, QMainWindow, QPushButton
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6 import QtTest 

# Unitest area
import unittest
from time import sleep
def sleep_sec(sec):
    sleep(10*sec)
#
#define class for unittest
class Test(unittest.TestCase):
    def test_square(self):
        sleep_sec(5)

class Ui_Dialog(object):
    def setupUi(self, Dialog):
        Dialog.setObjectName("Dialog")
        Dialog.resize(640, 480)
        self.verticalLayout = QtWidgets.QVBoxLayout(Dialog)

    def retranslateUi(self, Dialog):
        _translate = QtCore.QCoreApplication.translate

class Window(QMainWindow):
    """Main window."""
    def __init__(self, parent=None):
        """Initializer."""
        super().__init__(parent)
        # Use a QPushButton for the central widget
        self.centralWidget = QPushButton("Test_Button")

        # Connect the .clicked() signal with the .onTest_BtnClicked() slot
        self.centralWidget.clicked.connect(self.onTest_BtnClicked)
        self.setCentralWidget(self.centralWidget)

    # Create a slot for launching the Test_ dialog
    def onTest_BtnClicked(self):
        """Launch the Test_ dialog."""
        dlg = Test_Dlg(self)
        # This will test with QtTest just for click 
        QtTest.QTest.qWait(2500)

        dlg.exec()

class Test_Dlg(QDialog):
    """Test dialog."""
    def __init__(self, parent=None):
        super().__init__(parent)
        # Create an instance of the GUI
        self.ui = Ui_Dialog()
        # Run the .setupUi() method to show the GUI
        self.ui.setupUi(self)

if __name__ == "__main__":
    # this test all run of application and show :
    # Ran 1 test in 50.001s
    # uncoment this
    #unittest.main()
    
    # Create the application
    app = QApplication(sys.argv)
    # Create and show the application's main window
    win = Window()
    win.show()
    # Run the application's main loop
    sys.exit(app.exec())

Wednesday, February 7, 2024

Python 3.12.1 : Simple view for sqlite table with PyQt6.

This is the source code that show you how to use QSqlDatabase, QSqlQuery, QSqlTableModel:
import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView
from PyQt6.QtSql import QSqlDatabase, QSqlQuery, QSqlTableModel

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        # Initialize the database
        self.init_db()

        # Set up the GUI
        self.table_view = QTableView(self)
        self.setCentralWidget(self.table_view)

        # Set up the model and connect it to the database
        self.model = QSqlTableModel(self)
        self.model.setTable('files')
        self.model.select()
        self.table_view.setModel(self.model)

    def init_db(self):
        # Connect to the database
        db = QSqlDatabase.addDatabase('QSQLITE')
        db.setDatabaseName('file_paths.db')
        if not db.open():
            print('Could not open database')
            sys.exit(1)

    def create_table(self):
        # Create the 'files' table if it doesn't exist
        query = QSqlQuery()
        query.exec('CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY, path TEXT)')

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

Sunday, June 11, 2023

Python Qt6 : Download for youtube with PyQt6.

Simple example with PyQt6 to create an interface to download a video using a URL from youtube.
This simple example has some limitations, the filtering of the results is done according to the possibilities of the pytube mode and only after video, it does not use multithread and it does not have multiple selection possibilities and options. In conclusion, it offers a simple download functionality.
You can see more on my GitHub account.
import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QProgressBar, QDialog, QComboBox, QLabel, QMessageBox
from PyQt6.QtGui import QIcon, QPixmap
from pytube import YouTube
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QDialogButtonBox


class FormatChecker:
    def __init__(self, url):
        self.url = url

    def check_formats(self):
        try:
            yt = YouTube(self.url)
            formats = []
            streams = yt.streams.filter(only_video=True)
            for stream in streams:
                if stream.url:
                    format_info = {
                        'resolution': stream.resolution,
                        'file_extension': stream.mime_type.split("/")[-1]
                    }
                    formats.append(format_info)
                    print(" format_info ",format_info)
            return formats
        except Exception as e:
            print("Error:", str(e))
            return []


class FormatInfo:
    def __init__(self, resolution, file_formats):
        self.resolution = resolution
        self.file_formats = file_formats


class ResolutionDialog(QDialog):
    def __init__(self, formats, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Select Resolution and File Format")
        self.formats = formats

        layout = QVBoxLayout(self)

        self.resolution_combo = QComboBox(self)
        for format_info in formats:
            resolution = format_info.resolution
            self.resolution_combo.addItem(resolution)
        layout.addWidget(self.resolution_combo)

        self.file_format_combo = QComboBox(self)
        self.update_file_formats(self.resolution_combo.currentText())
        layout.addWidget(self.file_format_combo)

        button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
        button_box.accepted.connect(self.accept)
        button_box.rejected.connect(self.reject)
        layout.addWidget(button_box)

        self.resolution_combo.currentIndexChanged.connect(self.on_resolution_changed)

    def update_file_formats(self, resolution):
        self.file_format_combo.clear()
        for format_info in self.formats:
            if format_info.resolution == resolution:
                file_formats = format_info.file_formats
                self.file_format_combo.addItems(file_formats)

    def selected_resolution(self):
        return self.resolution_combo.currentText()

    def selected_file_format(self):
        return self.file_format_combo.currentText()

    def on_resolution_changed(self, index):
        resolution = self.resolution_combo.currentText()
        self.update_file_formats(resolution)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("YouTube Downloader - selected - only_video =True")
        self.setFixedWidth(640)

        central_widget = QWidget(self)
        self.setCentralWidget(central_widget)

        layout = QVBoxLayout(central_widget)

        self.url_edit = QLineEdit()
        layout.addWidget(self.url_edit)

        download_button = QPushButton("Download")
        download_button.clicked.connect(self.show_resolution_dialog)
        layout.addWidget(download_button)

        progress_layout = QHBoxLayout()
        layout.addLayout(progress_layout)

        self.progress_bar = QProgressBar()
        self.progress_bar.setTextVisible(True)
        progress_layout.addWidget(self.progress_bar)

        self.progress_icon_label = QLabel(self)
        pixmap = QPixmap("youtube.png")  # Înlocuiți "path_to_icon.png" cu calea către iconul dorit
        self.progress_icon_label.setPixmap(pixmap)
        progress_layout.addWidget(self.progress_icon_label)

    def show_resolution_dialog(self):
        url = self.url_edit.text()
        if url:
            format_checker = FormatChecker(url)
            formats = format_checker.check_formats()
            format_infos = []
            for format in formats:
                resolution = format['resolution']
                file_extension = format['file_extension']
                format_info = next((info for info in format_infos if info.resolution == resolution), None)
                if format_info:
                    format_info.file_formats.append(file_extension)
                else:
                    format_info = FormatInfo(resolution, [file_extension])
                    format_infos.append(format_info)

            dialog = ResolutionDialog(format_infos, self)
            if dialog.exec() == QDialog.DialogCode.Accepted:
                resolution = dialog.selected_resolution()
                file_format = dialog.selected_file_format()
                self.download_video(url, resolution, file_format)
        else:
            print("Please enter a valid YouTube URL.")

    def download_video(self, url, resolution, file_format):
        try:
            yt = YouTube(url)
            stream = yt.streams.filter(only_video=True, resolution=resolution, mime_type="video/" + file_format).first()
            if stream:
                stream.download()
                print("Download completed!")
                QMessageBox.question(self, "Download Completed", "The video has been downloaded successfully.", QMessageBox.StandardButton.Ok)
            else:
                print("Error: The selected video format is not available for download.")
                QMessageBox.question(self, "Download Error", "The selected video format is not available for download.", QMessageBox.StandardButton.Ok)
        except Exception as e:
            print("Error:", str(e))
            QMessageBox.question(self, "Download Error", "An error occurred during the download.", QMessageBox.StandardButton.Ok)


def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()

Monday, April 17, 2023

Python Qt6 : use sqlite - part 002.

In this article tutorial I will show you how to read from the sqlite file the content of the table: files.
In the last article tutorial, I create a interface with PyQt6 that search files by regular expresion and result is add to sqlite file named: file_paths.db.
I used same steps with a default python class and I used QSqlTableModel to show the content received.
The script will create a window with this QSqlTableModel, then reads the file and add the result.
Let's see the source code:
import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView
from PyQt6.QtSql import QSqlDatabase, QSqlQuery, QSqlTableModel

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        # Initialize the database
        self.init_db()

        # Set up the GUI
        self.table_view = QTableView(self)
        self.setCentralWidget(self.table_view)

        # Set up the model and connect it to the database
        self.model = QSqlTableModel(self)
        self.model.setTable('files')
        self.model.select()
        self.table_view.setModel(self.model)

    def init_db(self):
        # Connect to the database
        db = QSqlDatabase.addDatabase('QSQLITE')
        db.setDatabaseName('file_paths.db')
        if not db.open():
            print('Could not open database')
            sys.exit(1)

    def create_table(self):
        # Create the 'files' table if it doesn't exist
        query = QSqlQuery()
        query.exec('CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY, path TEXT)')

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())
I add this source code into a python script named view.py and I run it.
This is the result of the running script:

Python Qt6 : use sqlite - part 001.

This will default update for any python project:
python.exe -m pip install --upgrade pip --user
...
Successfully installed pip-23.1
The sqlite3 is already on my python instalation because I use version 3.11.0, you can see the official webpage.
Install the PyQt6 with the pip tool, I have this python package:
pip install PyQt6 --user
Requirement already satisfied: PyQt6 in c:\python311\lib\site-packages (6.4.1)
...
The next source of code will create a windows with two buttons and one edit area.
The PyQt6 graphics user interface use these elements: QPushButton, QLineEdit and QMessageBox from QWidget.
The python class will create a window with these elements and dor each of these is need to have methods.
First you need to select the folder, then use an regular expresion for search.
I used this : .*\.blend1$ this means *.blend1.
The last step is to use FindFiles button to search all blend files, in this case and add path of each of these to the sqlite database into a table named: files .
If you select the root C: then will take some time to search the files.
Let's see the source code:
import sys
import os
import re
from PyQt6.QtWidgets import *
from PyQt6.QtCore import *
import sqlite3

class FindFiles(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Find Files")
        self.setGeometry(100, 100, 500, 300)

        self.folder_button = QPushButton("Choose Folder")
        self.folder_button.clicked.connect(self.choose_folder)
        self.pattern_edit = QLineEdit()
        self.pattern_edit.setPlaceholderText("Enter regular expression pattern")
        self.pattern_edit.setFixedWidth(250)
        self.find_button = QPushButton("Find Files")
        self.find_button.clicked.connect(self.find_files)

        layout = QVBoxLayout()
        layout.addWidget(self.folder_button)
        layout.addWidget(self.pattern_edit)
        layout.addWidget(self.find_button)
        self.setLayout(layout)

        self.folder_path = ""

        self.conn = sqlite3.connect("file_paths.db")
        self.cursor = self.conn.cursor()
        self.cursor.execute("CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY, path TEXT)")

    def choose_folder(self):
        self.folder_path = QFileDialog.getExistingDirectory(self, "Choose Folder")
        if self.folder_path:
            self.folder_button.setText(self.folder_path)

    def find_files(self):
        if not self.folder_path:
            QMessageBox.warning(self, "Warning", "Please choose a folder first!")
            return

        pattern = self.pattern_edit.text()

        if not pattern:
            QMessageBox.warning(self, "Warning", "Please enter a regular expression pattern!")
            return

        file_paths = []
        for root, dirs, files in os.walk(self.folder_path):
            for file in files:
                if re.match(pattern, file):
                    file_path = os.path.join(root, file)
                    file_paths.append(file_path)
                    self.cursor.execute("INSERT INTO files (path) VALUES (?)", (file_path,))
        self.conn.commit()

        QMessageBox.information(self, "Information", f"Found {len(file_paths)} files that match the pattern!")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    find_files = FindFiles()
    find_files.show()
    sys.exit(app.exec())
I put this source code into a file named:main.py and I run it.
python main.py
The result is this:

Saturday, January 28, 2023

Python : Fix error with pip and pylupdate6.exe .

I try to upgrade the PyQt6 package and I got this error:

pip3 install --upgrade --force-reinstall PyQt6
...
ERROR: Could not install packages due to an OSError: [WinError 2] The system cannot find the file specified:
...pylupdate6.exe
...pylupdate6.exe.deleteme'
I think this solution will work for any python package
Open a command shell run as administrator and run it again:
pip3 install --upgrade --force-reinstall PyQt6
Collecting PyQt6
...
Successfully installed PyQt6-6.4.1 PyQt6-Qt6-6.4.2 PyQt6-sip-13.4.1
If you want to re-download the packages instead of using the files from your pip cache, then use:
pip install --force-reinstall --no-cache-dir

Tuesday, January 24, 2023

Python : Fix DLL load failed while importing ...

This error DLL load failed while importing ... can have many causes like conflicts with the already installed packages, and also it can break your current environment.
>>> import PyQt6
>>> from PyQt6.QtCore import QUrl
Traceback (most recent call last):
  ...
ImportError: DLL load failed while importing QtCore: The specified module could not be found.
You can see I used to reinstall the PyQt6 python package with this argument --ignore-installed:
pip3 install PyQt6 --user --ignore-installed
Collecting PyQt6
  ...
Installing collected packages: PyQt6-Qt6, PyQt6-sip, PyQt6
  WARNING: The scripts pylupdate6.exe and pyuic6.exe are installed in
  ...
  which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
Successfully installed PyQt6-6.4.0 PyQt6-Qt6-6.4.2 PyQt6-sip-13.4.0
The --ignore-installed option for the pip package manager was first introduced in version 6.0, which was released in April 2014.
The old install give me this error and when I try to use I got this:
>>> from PyQt6 import *
>>> dir(PyQt6)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__']
Now after I reinstall with this option the result is good:
>>> import PyQt6
>>> from PyQt6 import QtCore
>>> dir(PyQt6)
['QtCore', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'sip']
>>> from PyQt6.QtCore import QUrl

Thursday, January 5, 2023

Python Qt6 : Create a tray icon application.

The notification area known as system tray is located in the Windows Taskbar area, usually at the bottom right corner.
I tested with:
Python 3.10.2 (tags/v3.10.2:a58ebcc, Jan 17 2022, 14:12:15) [MSC v.1929 64 bit (AMD64)] on win32
...
pip show pyqt6
Name: PyQt6
Version: 6.4.0
Summary: Python bindings for the Qt cross platform application toolkit
I created a trayicon application with PyQt6 that show a menu with two entry: Show Window and Exit.
The Show Window will sow a default window and the Exit will close the application.
This is the source code I tested, you need an icon.png in the same folder with this script.
import sys
from PyQt6.QtCore import Qt, QEvent, QPoint
from PyQt6.QtGui import QGuiApplication, QIcon, QAction
from PyQt6.QtWidgets import QApplication, QSystemTrayIcon, QMainWindow, QMenu

# create the default application
app = QApplication(sys.argv)

# create the main window
window = QMainWindow()
# set the title for the window
window.setWindowTitle("My Window")

# this set the tray icon and menu
tray_icon = QSystemTrayIcon()
tray_icon.setIcon(QIcon("icon.png"))
menu = QMenu()

# add an action to the menu to show the window
show_window_action = QAction("Show Window", None)
show_window_action.triggered.connect(window.show)
menu.addAction(show_window_action)

# add an action to the menu to exit the application
exit_action = QAction("Exit", None)
exit_action.triggered.connect(app.quit)
menu.addAction(exit_action)

# set the context menu
tray_icon.setContextMenu(menu)

# show the tray icon
tray_icon.show()

# run the application
app.exec()

Tuesday, January 3, 2023

Python 3.10.2 : about ChemSpiPy.

ChemSpiPy provides a way to interact with ChemSpider in Python. It allows chemical searches, chemical file downloads, depiction and retrieval of chemical properties...
You can read more about this python package on the official website and the documentation for this python package.
You have to create an account and fill in the data to get an A.P.I key...
This is the source code for this python package , I use PyQt6 to show image formula with:QPixmap.fromImage.
import chemspipy

# set the API key from https://developer.rsc.org/my-apps/
api_key = "... your A.P.I. key ..."

# import the ChemSpider class
from chemspipy import ChemSpider

# instance of the ChemSpider class
cs_inst = ChemSpider(api_key)

compound_name = "Glucose"
# search for compound: "Glucose"
compounds  = cs_inst.search(compound_name) 

# get the first compound in the list
compound = compounds[0]

# print the list of the attributes and methods of 'compound' object
print(dir(compound))

# Print the compound's properties
print("... some compound's properties !")
print(f"Name: {compound.common_name}")
print(f"Average_mass: {compound.average_mass}")
print(f"ChemSpider ID - csid: {compound.csid}")
#print(f" external_references: {compound.external_references}")
#print(f" image: {compound.image}")
#print(f" mol_2d: {compound.mol_2d}")
#print(f" mol_3d: {compound.mol_3d}")
print(f" molecular_formula: {compound.molecular_formula}")
print(f" molecular_weight: {compound.molecular_weight}")
print(f" monoisotopic_mass: {compound.monoisotopic_mass}")
print(f" nominal_mass: {compound.nominal_mass}")


import sys
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPixmap, QImage, QColor
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel

# Create the application
app = QApplication(sys.argv)

# Create the main window
window = QMainWindow()
window.setWindowTitle(compound_name)

# Load the image with fromData
image = QImage.fromData(compound.image)

# Create a label to display the image
label = QLabel()
label.setPixmap(QPixmap.fromImage(image))

# Set the label as the central widget of the window
window.setCentralWidget(label)

# Show the window
window.show()

# Run the application
app.exec()
This is the result of running the source code:

Monday, January 2, 2023

Python Qt6 : Show any CSV file with QTableWidget.

In this tutorial, I will show you how easy it is to work with PyQt6 and QTableWidget to display any CSV file.
The source code lines are already commented to understand how this source code works.
This is the content of the csv file named my.csv:
id,firstname,lastname,email,email2,profession
100,Maud,Callista,Maud.Callista@yopmail.com,Maud.Callista@gmail.com,police officer
101,Justinn,Rona,Justinn.Rona@yopmail.com,Justinn.Rona@gmail.com,worker
102,Gabriellia,Robertson,Gabriellia.Robertson@yopmail.com,Gabriellia.Robertson@gmail.com,police officer
103,Gwenneth,Payson,Gwenneth.Payson@yopmail.com,Gwenneth.Payson@gmail.com,firefighter
104,Lynea,Robertson,Lynea.Robertson@yopmail.com,Lynea.Robertson@gmail.com,developer
This is the source I used:
import sys
import csv
from PyQt6.QtWidgets import QApplication, QMainWindow, QTableWidget, QTableWidgetItem
from PyQt6.QtCore import Qt

# the main window class
class MainWindow(QMainWindow):
    # the init definition of the class
    def __init__(self):
        super().__init__()

        # the window title
        self.setWindowTitle('Table Viewer for any CSV file.')

        # this create the table widget
        self.table = QTableWidget(self)

        # the table dimensions is set default
        self.table.setColumnCount(0)
        self.table.setRowCount(0)

        # this read the CSV file named my.csv
        with open('my.csv', 'r') as file:
            # use the reader for file 
            reader = csv.reader(file)
            # this get the column labels from the first row
            headers = next(reader)
            # this set the number of columns based on the number of headers
            self.table.setColumnCount(len(headers))
            # this set the horizontal header labels
            self.table.setHorizontalHeaderLabels(headers)
            # for each row iterate the rows in the CSV file
            for row in reader:
                # add a row to the table
                row_index = self.table.rowCount()
                self.table.insertRow(row_index)
                # add data to cells 
                for col_index, cell in enumerate(row):
                    self.table.setItem(row_index, col_index, QTableWidgetItem(cell))
        
        # setting for the table as the central widget
        self.setCentralWidget(self.table)

# this run the application
if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())
Here is what the result of running the source code:

Monday, January 3, 2022

Python Qt6 : The basic differences between PyQt5 and PyQt6.

Python Qt6 known as PyQt6 is a binding of the cross-platform GUI toolkit Qt, implemented as a Python plug-in with Qt 6 the latest version of Qt.
The PyQt6 first stable release was on 6 January 2021, developed by Riverbank Computing Ltd and the last release was on 2 December 2021 with the version PyQt v6.2.2.
This release is license GPL or commercial on Python 3 platform.
Let's see some differences between PyQt5 and PyQt6.
The .exec() method is used in Qt to start the event loop of your QApplication or dialog boxes. In Python 2.7 exec was a keyword and Python 3 removed the exec keyword.
The first difference as a result of PyQt6 .exec() calls are named just as in Qt.
If you read the documentation from the official webpage, then in your PyQt6 source code you need to use these changes:
QEvent.Type.MouseButtonPress
...
Qt.MouseButtons.RightButton
...
Both PyQt5 and PyQt6, although seemingly easy to use for complex applications, will require extra effort.

Saturday, July 17, 2021

Python Qt6 : Install and use python with Visual Studio.

Visual Studio is a very good tool for python programming language development.
Today I will show you how to use it with Visual Studio on a Windows operating system.
If you don't have Python install then start the Visual Studio installer and from all presents select the Python development workload.
Start Visual Studio and open a folder or open an empty file and save with the python language-specific extension: py.
Select the Python environment and add the new package with pip tool.
This is the python script I used to test:
import sys
from PyQt6.QtWidgets import QApplication, QWidget

def main():

    app = QApplication(sys.argv)

    w = QWidget()
    w.resize(250, 200)
    w.move(300, 300)

    w.setWindowTitle('Simple')
    w.show()

    sys.exit(app.exec())

if __name__ == '__main__':
    main()
You can see the video tutorial about how you can use it:

Tuesday, July 6, 2021

Python Qt6 : First example on Fedora 34 Linux distro.

Qt for Python is the project that provides the official set of Python bindings (PySide6) that will supercharge your Python applications. While the Qt APIs are world renowned, there are more reasons why you should consider Qt for Python.
I tested with Fedora 34 Linux distro:
[root@desk mythcat]# dnf search PyQt6
Last metadata expiration check: 2:03:10 ago on Tue 06 Jul 2021 08:52:41 PM EEST.
No matches found. 
First stable release for PyQt6 was on Jan 2021 by Riverbank Computing Ltd. under GPL or commercial and can be used with Python 3.
Let's install with pip tool:
[mythcat@desk ~]$ /usr/bin/python3 -m pip install --upgrade pip
...
  WARNING: The scripts pip, pip3 and pip3.9 are installed in '/home/mythcat/.local/bin' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, 
  use --no-warn-script-location.
Successfully installed pip-21.1.3
[mythcat@desk ~]$ pip install PyQt6 --user
...
Let's see a simple example with this python package:
import sys
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QLabel

def main():
    app = QApplication(sys.argv)
    win = QLabel()
    win.resize(640, 498)
    win.setText("Qt is awesome!!!")
    win.show()
    app.exec()

if __name__ == "__main__":
    main()
I tested and run well.