Want to Get Ooey GUI with PyQt6, Zeep, and AXL?


You’re a Python aficionado. Want to build a Python AXL administration and configuration app with a GUI? Have I got the solution for you. No, it’s not Tkinter or wxWidgets, which is based on the Gimp Toolkit (GTK). We’re talking Qt, one of the most popular frameworks for multiple platforms. wxWidgets has similar features, but I’ve never been a fan of GTK.

Why not just build an app with a web interface?

Good question. Way back when I was editor-in-chief of Linux Journal, there was a big push for an Outlook clone called Evolution. Evolution is a C# app on Mono, a Linux clone of .NET. I considered this a misguided effort and said as much. It’s a fool’s errand to perpetually chase feature parity with Microsoft’s offerings. And back then when Internet dinosaurs roamed the earth, the world was already moving toward Software as a Service (SaaS) for user-facing applications. If you like Evolution, more power to you, but even Microsoft has come around to SaaS. The Office 365 web-based suite is excellent. I can use my work Outlook account on the web. And I use my personal Office 365 Outlook account from within a web browser on Windows and Linux. Instant platform-neutral access to Outlook and no need for Mono’s nucleosis.

A handy Python SOAP AXL interface with a modern Qt UI

As much as I pushed SaaS back then, here’s where I part ways. AXL is an administration and configuration API for Cisco UCM, not an API for user-facing apps. The way I see it, if you want to do administration of CUCM on the web, there’s the web GUI that comes with CUCM. When you want a custom administration app, I prefer an AXL application with a GUI native to the OS. Qt is a GUI framework that works on Windows, Mac, and Linux. Best of all, Monty’s pinnacle of achievement, Python, runs on all these platforms, and there’s a Python version of Qt called PyQt. Throw in a little Zeep, and you’ve got a handy Python SOAP AXL interface with a modern Qt UI.

Here’s how I start

All of the following examples are on Windows. Some of the commands will be different on a Mac or Linux, but it all works. If you want to take a gander at the finished sample app, you can get it from GitHub. I chose version 6, PyQt6 for this app, but you can use earlier versions and adjust the code accordingly. I’m using a generic “>” command line prompt. Your prompt will be different. If coding in Python with Zeep is new to you, or you want more details on differences between platforms, check out this learning lab.

First, I create a virtual Python environment.

>python -m venv PyQtAXL

I create a Project directory, change to the directory and activate the environment. Then I install the libraries we need.

>cd PyQtAXL
>mkdir Project
>cd Project
>..Scriptsactivate
>pip install zeep
>pip install PyQt6

I get a message that pip isn’t the latest version, so I update it.

>python -m pip install --upgrade pip

Now I’m ready to start coding a simple getUser app, which I’ll call GetUser.py.

Here’s what the app looks like when you first launch it (I entered my userid):

The PyQt6 App ready to search for a user

I click “Search Username” and see this:

The Search Results

Click “Reset” and poof, the info disappears. (You could resize the window, too, but I didn’t bother.)

Back to the Search screen

The nice thing about PyQt6 is that all I closed with the Reset button is the “Results” group box. That object goes away, along with all the child objects inside the box, including even the reset button. So, you can click “Search Username” and “Reset” as many times as you like and not experience any ill effects.

Now let’s look at the code. Don’t copy and paste anything that follows. There’s a lot more to this app than I’m showing here. We’ll see the entire app later. This section is an abbreviated explanation of how it works.

The AXL request looks like this:

    criteria = {
        'userid' : userid
    }
    response = self.service.getUser(**criteria)

If you want to see the entire JSON response, add a print(response) in your code and the entire response will show in the CMD window where you run the app. For our purposes, I will show only the data we plan to use:

{
    'return': {
        'user': {
            'firstName': 'nicholas',
            'displayName': 'nicholas petreley',
            'middleName': None,
            'lastName': 'petreley',
            'userid': 'nicholas',
            'associatedDevices': {
                'device': [
                    'CSFNicholas',
                    'SEP2C31246D2458',
                    'SEP5475D0785AD4'
                ]
            }…

Let’s do some Python to get the user values we want to show (we’ll get fancy with this later when we use PyQt6 – this is just straight Python, extracting data from the dictionary and list in the above JSON):

        d = response['return']['user']
        fname = d['firstName']
        lname = d['lastName']
        dname = d['displayName']

        ad = response['return']['user']['associatedDevices']['device']
        for value in ad:
            values[ad.index(value)] = value

Now we need some way to display all this data in a GUI app using the Qt framework. We’ll create a window with an input field where you will type a username, and a button that tells the app to search for that user. Like any good platform-neutral framework, Qt uses layouts. Layouts make it possible for an app to look good without having to specify the location of objects in X,Y coordinates or specify sizes. In this case, we’ll use a horizontal layout, with the search button to the left of the input field.

class MainWindow(QWidget):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.initUI()

    def initUI(self):
        button = QPushButton("Search Username")
        self.input = QLineEdit()

        hbox = QHBoxLayout()
        hbox.addWidget(button)
        hbox.addWidget(self.input)
        self.input.setText('nicholas')
        hbox.addStretch(1)

        button.clicked.connect(self.search_user)

As you can see, I preload the QLineEdit field, self.input.setText('nicholas'), with my name so I don’t have to type it when I run the app. The sample app doesn’t have that line of code.

Note that I connect the button clicked event to a method called self.search_user. More on that later. Right now, we need a place to put the search results and a way to wrap this up with a window title. Let’s create a vertical box layout for the results, add the title, and then do the obligatory “show” command so you can see what we’ve done.

        self.vbox = QVBoxLayout()
        self.vbox.addLayout(hbox)
        self.vbox.addStretch(1)

        self.setLayout(self.vbox)
        self.setWindowTitle("getUser")
        self.show()

We’re going to show the search results in the method called “self.search_user”. To make the results fancy pants, we’ll put them in one “results” group box that contains two more group boxes; one for the name data, the other for the associated devices.

        self.resBox = QGroupBox("Results")
…
        self.groupBox = QGroupBox("User")
…
        self.adGroup = QGroupBox("Associated Devices")

Each box has its own layout. The results box will be a simple layout that puts its children in a vertical stack.

        self.resBox = QGroupBox("Results")
        self.vresBox = QVBoxLayout()
        self.resBox.setLayout(self.vresBox)

We want labels to the left of values in the name data, so we’ll use a grid layout with two columns for that. We specify the columns when we add the data, not when we define the grid.

        self.groupBox = QGroupBox("User")
        self.gridBox = QGridLayout()
        self.groupBox.setLayout(self.gridBox)

When we put the data in that grid, we specify the row and column for labels and data. The first label, self.fl, goes in row 0, column 0. The first data field self.fe goes in row 0, column 1. And so on.

        self.fl = QLabel('First Name')
        self.fe = QLineEdit(d['firstName'])
        self.ll = QLabel('Last Name')
        self.le = QLineEdit(d['lastName'])
        self.dl = QLabel('Display Name')
        self.de = QLineEdit(d['displayName'])

        self.groupBox.setLayout(self.gridBox)
        self.gridBox.addWidget(self.fl,0,0)
        self.gridBox.addWidget(self.fe,0,1)
        self.gridBox.addWidget(self.ll,1,0)
        self.gridBox.addWidget(self.le,1,1)
        self.gridBox.addWidget(self.dl,2,0)
        self.gridBox.addWidget(self.de,2,1)

The list of associated devices is a simply vertical list box. We’ll make them all QLineEdit fields.

        self.adGroup = QGroupBox("Associated Devices")
        self.advbox = QVBoxLayout()
        self.adGroup.setLayout(self.advbox)

        for value in ad:
            values[ad.index(value)] = QLineEdit(value)
            self.advbox.addWidget(values[ad.index(value)])

Now let’s add the reset button and connect it to the self.resetBox method.

        self.resetButton = QPushButton("Reset")
        self.resetButton.clicked.connect(self.resetBox)

Now we embed all the widgets where they go. We add the name data box self.groupBox to the vertically oriented results box, self.vresBox, which we defined above. We add the associated devices box self.adGroup to the same self.vresBox. We add the reset button to the results box self.vresBox. And to wrap things up, we add the results box self.resBox to the main UI vertical box we defined much earlier, self.vbox. Tell the app to show what we have, and voila, we see the results screen.

        self.vresBox.addWidget(self.groupBox)
        self.vresBox.addWidget(self.adGroup)
        self.vresBox.addWidget(self.resetButton)
        self.vbox.addWidget(self.resBox)
        self.show()

Now we just need to add the method that resets the results:

    def resetBox(self):
        self.resBox.close()

As you can see, all we have to close is the results group box that contains all the results data children.

Now it’s time to see the whole enchilada, including the Zeep SOAP code. You’ll see that things aren’t exactly in the above order. The above examples are meant to show how PyQt6 works. In actual practice, the code can be shuffled around as desired. Also, using “import *” is frowned upon in Python, but I used it with PyQt6 in this code to make it simpler to read. When you write your own app, you should specify the widgets and objects you intend to use from the libraries.

from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from PyQt6.QtCore import *
from lxml import etree
from requests import Session
from requests.auth import HTTPBasicAuth
from zeep import Client, Settings, Plugin
from zeep.transports import Transport
from zeep.cache import SqliteCache

import argparse

import sys

# Parse command line arguments. 
# Overwrite them with values for your own CUCM using or use command line args

def parse_args():

    parser = argparse.ArgumentParser()
    parser.add_argument('-u', dest="username", help="AXL username", required=False, default="administrator_username")
    parser.add_argument('-p', dest="password", help="AXL user password", required=False, default="administrator_password")
    parser.add_argument('-s', dest="server", help="CUCM hostname or IP address", required=False,default="your_ucm_server")
    args = parser.parse_args()
    return vars(args)

class MainWindow(QWidget):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.initUI()

        # The WSDL is a local file
        WSDL_URL = 'AXLAPI.wsdl'

        # Parse command line arguments
        cmdargs = parse_args()
        USERNAME, PASSWORD, SERVER = (
            cmdargs['username'], cmdargs['password'], cmdargs['server'] )
        CUCM_URL = 'https://' + SERVER + ':8443/axl/'

        # This is where the meat of the application starts
        # The first step is to create a SOAP client session
        session = Session()

        # We avoid certificate verification by default, but you can set your certificate path
        # here instead of the False setting
        session.verify = False
        session.auth = HTTPBasicAuth(USERNAME, PASSWORD)

        transport = Transport(session=session, timeout=10, cache=SqliteCache())

        # strict=False is not always necessary, but it allows zeep to parse imperfect XML
        settings = Settings(strict=False, xml_huge_tree=True)

        client = Client(WSDL_URL, settings=settings, transport=transport)

        self.service = client.create_service("{http://www.cisco.com/AXLAPIService/}AXLAPIBinding", CUCM_URL)

    def initUI(self):

        button = QPushButton("Search Username")
        self.input = QLineEdit()

        hbox = QHBoxLayout()
        hbox.addWidget(button)
        hbox.addWidget(self.input)
        self.input.setText('nicholas')

        button.clicked.connect(self.search_user)

        self.vbox = QVBoxLayout()
        self.vbox.addLayout(hbox)
        self.vbox.addStretch(1)

        self.setLayout(self.vbox)
        self.setWindowTitle("getUser")
        self.show()

    def search_user(self):
        userid = self.input.text()
        criteria = {
            'userid' : userid
        }
        response = self.service.getUser(**criteria)

        self.resBox = QGroupBox("Results")
        self.vresBox = QVBoxLayout()
        self.resBox.setLayout(self.vresBox)

        d = response['return']['user']
        self.fl = QLabel('First Name')
        self.fe = QLineEdit(d['firstName'])
        self.ll = QLabel('Last Name')
        self.le = QLineEdit(d['lastName'])
        self.dl = QLabel('Display Name')
        self.de = QLineEdit(d['displayName'])

        self.groupBox = QGroupBox("User")
        self.gridBox = QGridLayout()
        self.groupBox.setLayout(self.gridBox)
        self.gridBox.addWidget(self.fl,0,0)
        self.gridBox.addWidget(self.fe,0,1)
        self.gridBox.addWidget(self.ll,1,0)
        self.gridBox.addWidget(self.le,1,1)
        self.gridBox.addWidget(self.dl,2,0)
        self.gridBox.addWidget(self.de,2,1)

        self.adGroup = QGroupBox("Associated Devices")
        self.advbox = QVBoxLayout()
        self.adGroup.setLayout(self.advbox)

        ad = response['return']['user']['associatedDevices']['device']
        values = dict()
        for value in ad:
            values[ad.index(value)] = QLineEdit(value)
            self.advbox.addWidget(values[ad.index(value)])

        self.resetButton = QPushButton("Reset")
        self.resetButton.clicked.connect(self.resetBox)

        self.vresBox.addWidget(self.groupBox)
        self.vresBox.addWidget(self.adGroup)
        self.vresBox.addWidget(self.resetButton)
        self.vbox.addWidget(self.resBox)
        self.show()

    def resetBox(self):
        self.resBox.close()

def main():

    app = QApplication(sys.argv)
    su = MainWindow()
    sys.exit(app.exec())

if __name__ == '__main__':
    main()

app.exec()

You can grab the above sample code here. Make sure to download the AXL SQL toolkit from UCM and put the appropriate WSDL and XSD files in the same directory as the app. Instructions on how to do that are included in the Python/Zeep Learning Lab.

Once you get the hang of using PyQt6, it’s a very pleasing API to work with. You’ll quickly develop complex prototype applications that you can promote to production apps almost as fast.

Resources:

PyQt6 Documentation

Python/Zeep Learning Lab

GetUser.py sample app on GitHub

Share:



Source link