Control the web with Python 🐍
To install a stable version:
pip install purlyTo install a dev version:
be sure to install
npmfirst!
# clone the repository
git clone https://github.com/rmorshea/purly
cd purly && bash scripts/build.sh && bash scripts/install.shRun the following snippet of code, and then navigate to http://127.0.0.1:8000/model/index.
import purly
# Prepare your layout
purly.state.Machine().run(debug=False)
layout = purly.Layout('ws://127.0.0.1:8000/model/stream')
# create your HTML
div = layout.html('div')
div.style.update(height='20px', width='20px', background_color='coral')
# add it to the layout
layout.children.append(div)
# and sync it!
layout.sync()Now your creation should have magically appeared in the browser page you opened!
Purly's fundamental goal is to give Python as much control of a webpage as possible, and do so in one incredibly simple package. There is one major problem that stands in the way of this goal - data synchronization. Purly's answer to this problem is its model server which acts as a "source of truth" about the state of a webpage for any clients which connect to it and adhere to its protocol.
Purly uses a web socket server to keep multiple concurrent clients in sync. The animation above shows 2 clients - a Python client pushing updates to a single Browser - however you could have more clients producing and / or consuming, model updates. Each client is associated with a single model (any JSON serializable object), however there can be multiple models that are stored on the server. Clients connect to a particular model by specifying its name in the socket route (e.g. ws://host:port/model/<model-name>/stream). Only clients that are connected to the same model communicate with each other via the server.
While the Model Server supports any JSON serializable model, Purly, as a framework for controlling the web must:
- Communicate as fully as possible the structure of DOM elements and their various interactions.
- Send updates to DOM models over a network in short and easy to interpret packages:
- Update messages must be small in size in order to reduce network traffic.
To accomplish the goals defined above we propose a flat DOM model:
Model = {
id: Element,
# Maps a uniquely identifiable string to an Element.
root: Element,
# The id "root" should always indicate the outermost Element.
...
}Element = {
tagName: string
# Standard HTML tags like h1, table, div, etc.
signature: string
# The hash of this element attributes, and the hashes of its children.
children: [
string,
# Any arbitrary string.
{type: 'ref', 'ref': string},
# An object where the key "ref" refers to the "key" attribute.
...
],
attributes: {
key: id,
# The id that uniquely identifies this Element.
parent_key: id
# The unique id of this element's parent.
attr: value,
# Map any attribute name to any JSON serializable value.
on<Event>: {
# Specify an event callback with an attribute of the form "on<Event>".
callback: uuid,
# A unique identifier by which to refer to the callback function.
keys: [...],
# Details of the event to pass on to the callback.
update: [...]
# Any attributes that should be synced before the callback is triggered.
}
}
}The following HTML
<div key='root'>
Make a selection:
<input type='text' key='abc123'></input>
<div>would be communicated with the following Purly model:
{
root: {
tag: 'div',
elements: [
'Make a selection:'
{'ref': 'abc123'},
]
attributes: {
'key': 'root'
}
},
abc123: {
tag: 'input',
elements: [],
attributes: {
'key': 'abc123',
'type': 'text',
},
}
}The Purly model server sends and receives JSON serializable arrays which contain objects in the form of a Message.
[
Message,
# a dict conforming to the Message spec
...
]There are two types of messages - Updates and Signals - however both conform to the following format.
Message = {
"header": {
"type": "signal" or "update",
# Message type (indicates the kind of content).
"version": "0.1",
# Message protocol version.
}
# Content which depends on the message type.
"content": dict,
}- A Signal does not modify the state of the model server.
- Signal
contentis distributed unmodified to other model clients.
- The
contentfield specifies changes that will be merged into the model. - Only the differences between the Update
contentand the model are distributed to other clients. - Update merges are performed in a nested fashion
If the current state of the model is
{
'a': {
'b': 1,
'c': 1,
}
}and an update message
{
'a': {
'c': 2
}
}is received, the resulting model state is
{
'a': {
'b': 1,
'c': 2,
}
}
