A Python ate my GUI — Part 3: Implementation

Time to Bootstrap your D3, pick up that Python and hop on the Autobahn

If you aren’t aware of my earlier posts, check out Part 1 and Part 2 of this series so that you get some context for this ongoing exercise.

Since I started thinking about and working on these posts, I’ve also been developing the ideas on GitHub as a side project I called sofi.

Sofi is a Python 3 package that serves as the starter implementation of the design discussed in Part 2: a system that will generate the necessary HTML and JavaScript code typically needed to produce a single-page application and serve it up through WebSockets (not an HTTP server).

The webpage functions as a dumb user interface layer on top of your python code by exposing a simple command and event system that allows for communications back and forth with the python logic. The UI itself is generated using Bootstrap components, and enabled by D3.js for processing events and DOM changes, as well as for future integration with charting mechanisms.

Why did I do this?

I specifically wanted to understand and do more things with WebSockets. I like the technology and believe that it can enhance — possibly even replace — some of the RESTful interfaces that I build or maintain on a regular basis.

At the same time, I’ve been looking for an excuse to use asyncio and some of the more recent python 3 features, and found that the autobahn websocket module is built on top of it.

On another note, as discussed in a previous post, I’m also lazy! and tired of re-implementing web interfaces in languages that I would prefer not to use.

However, most importantly, I wanted to dive a little deeper into the implications of writing your own command / event protocol to enable GUI interactions, as I intend to use something similar in a different kind of project (which is a subject for another time).

How does it work?

The basic concept is fairly simple: statically serve an html file that references a minimal javascript library setup so that when the page loads, it automatically connects to the websocket server, updates the page DOM with whatever is needed and registers any relevant event subscriptions.

These DOM updates or event subscriptions are received as commands through the websocket, with enough information to specify the element selectors that are then passed onto D3 to perform the changes.

Whenever an event is triggered, it gets relayed back through the same socket. This includes page events (like click) as well as any control events necessary to establish the workflow (like init, load or close).

This socket protocol gives us the flexibility to host the static html anywhere we want and run the websockets themselves in some other place, while supporting communication with any operating system and any modern web browser (including Android and iOS).

To ease development of the user interface, common Bootstrap widgets are wrapped into python classes that allow for easy compositing through basic built-ins like __str__. Every widget object provides the basic set of methods necessary for generating the html that describes it, but also exposes the identifier, class, style and attributes fields to allow for further customization.

While these widgets give you a mechanism for rapidly prototyping an interface, they are not necessary and can be replaced with a simple string representing any HTML that you’d like to send up to the web page.

In order to allow for threaded processing of incoming events, any callback functions are expected as asyncio coroutines, otherwise the code would be unresponsive as long events are being processed.

Currently, the application as a whole is executed from a shell, where python automatically opens the default browser and points it to the static html starter file, with no need to run a webserver. However, it’s totally possible to start it and separately navigate to the html file from an already running browser.

Execution ends when the browser tab is closed and an event is sent back through the websocket to be handled appropriately, though I still need to improve that code to gracefully manage disconnects and coroutines.

What can you make?

Anything, of course! Kidding aside though, the system is pretty open to tie into any python program. At the moment you’ll be limited by the number of widgets available to represent your interface, and things are more verbose than I’d like, but all of that will eventually settle down.

The sample.py file in the main directory is a bit of a widget showcase that dynamically updates over time. It also has a click handler configured for the Primary button which will print out a message with the entire event object, making it clear what data is available to the callbacks.

Output from sample.py

The timeline.py file is an example of what can be done fairly quickly when tying into the greatness of the python ecosystem. It leverages the twitter api and outputs a bunch of bootstrap panels with each tweet from your main timeline.

Output from timeline.py

What’s next?

This is far from ready for prime time. There is still much work to be done to document, add more Bootstrap components, composite those into more useful widgets, add more commands to direct the display, adjust for less common events and input systems (i.e.: touch and gestures), add charts, add advanced table functionality, etc. However, before I get into all of that, I thought a little feedback will be helpful.

While I’m confident I can make something useful for myself, I’d like to make it at least a minimum viable product for everyone else.