Lomond is not the first websocket client for Python, so why go to the effort of building another one? For our purposes, we needed a stand alone client that didn't need a framework to run. So that excludes the websocket client support in Tornado, aiohttp etc. The two libraries that were suitable for our product, websocket-client and ws4py, both had show-stopper bugs with ssl support; websocket-client would sometimes refuse to processes packets until additional data was received, and ws4py could lose entire packets. I'm sure both libraries could be fixed, but neither project appears to be actively maintained.
Interestingly, both of those projects had a flaw in essentially the same area: processing data from SSL sockets. PSA – using select with SSL Socket objects may appear to work, but there can be unprocessed data in an internal buffer. It's hardly surprising that the authors of these two packages got that wrong; the problems it caused occurred only in production, and disappeared when we tried to reproduce it locally. It was a classic Heisenbug.
Fixing the SSL issue was the initial motivation, but Lomond was also an opportunity to make a websocket client library that was easier to use. Lomond takes an event based approach rather than callbacks, which—to my mind—is easier to reason about.
Here's an example of an echo client in Lomond, which connects to a server, and sends back any text messages it receives:
from lomond import WebSocket websocket = WebSocket('wss://echo.example.org') for event in websocket: if event.name == 'text': websocket.send_text(event.text)
These events are generated in an orderly manner, and no threads are used (even internally).
Another motivation was that we needed to build a client that would maintain a persistent reliable connection to a websocket server. In other words: It had to recover gracefully if connectivity is flakey. The libraries we used before Lomond did not make that easy. At one point we had a second thread monitoring the WebSocket library thread in order to reconnect if the socket dies. It was impossible to reason about fully, and not an approach I can see myself ever attempting again.
The linear event based approach taken by Lomond makes persistent connections rather straightforward. Here's how the above code could be adapted to make a persistent connection:
from lomond import WebSocket from lomond.persist import persist websocket = WebSocket('wss://echo.example.org') for event in persist(websocket): if event.name == 'text': websocket.send_text(event.text)
This one function replaced a lot of overly complex threaded code, which was deeply satisfying to remove.
Anything vaguely websockety was taken in PyPi, so I named it after a beautiful Scottish Loch.
The internals of Lomond separate logic from IO, with a coroutine based stream parser. This design opens up the possibility of integrating Lomond with async frameworks such as asyncio and curio. It is my hope that Lomond could become a reference implementation of sorts for Websocket clients.
Lomond is a young project, but it has near 100% test coverage and passes the Autobahn test suite. It is production ready, but I do expect there to be more features added before a
1.0.0 release. Top of my list is per-message compression.