Usage¶
To use mutaprops
in a project:
from mutaprops import *
Decorating classes¶
mutaprops
are created by decorating classes with some custom decorators,
much like the usage of the @property decorator. Those decorators enable the
property manager to see the properties and track their state.
- @mutaprop_class [
mutaprop_class()
] - is the mandatory class decorator which enable objects instantiated from the decorated classes to be tracked by the property manager.
- @mutaproperty [
mutaproperty()
] is the basic decorator. Any attribute decorated with
@mutaproperty
becomes visible in the UI. The arguments of@mutaproperty
allow to define the type of the parameter (that defines the UI widget), it’s display name, numerical ranges, the position/category in the UI etc.The docstring of the attribute is displayed as help in the UI. Any change to this attribute during run-time is reflected in the UI. Any change in the UI causes change of the attribute’s value.
Same as with usual properties,
@mutaproperty
can be read only, either by not defining a setter function, or by explicitly setting it toread_only
.Most parameters of the
@mutaproperty
can be assigned as a constant, or as a reference to another@mutaproperty
or@mutasource
. Change of a referred@mutaproperty
then causes UI update of the referee.All optional parameters can be specified either in getter or in setter decorator, it doesn’t matter which.
- @mutaprop_action [
mutaprop_action()
] - is basically a simplified version of
@mutaproperty
. It’s represented as a button on UI level, and causes direct function call of the decorated function when the button is pressed. - @mutasource [
mutasource()
] - is best described as a “hidden mutaproperty”.
@mutasource
is not directly visible, but it’s changes are propagated to the UI. It can be used to implement some controller (as in MVC) functionality which would be hard to do with@mutaproperty
alone.
All above-mentioned decorators wrap the original class implementation and add some extra functionality. None of it is used, however, if the instantiated objects are not registered with the UI manager. One therefore doesn’t need to be concerned about the functionality/performance impact of the decorators on the original class.
Setting up an UI manager¶
HttpMutaManager
does all the heavy-lifting,
providing data from registered object to the UI front-end and tracking
the object changes and user actions. It runs on aiohttp.
UI manager is basically a REST service (the UI front-end is a HTML5 app) with additional websocket component.
Apart of the bi-directional object data update, the manager is also responsible for forwarding the registered logger messages to the front-end, and also serving static files (the HTML5 app blob and custom user data).
Using external Asyncio loop¶
By default, the manager would create a new loop upon start. If the application
is using asyncio
as well, manager can be specified to use the
application’s loop.
import asyncio
from mutaprops import *
loop = asyncio.get_event_loop()
# Some nice async code
man = HttpMutaManager("Some manager", loop=loop)
Custom static files¶
UI manager can serve a custom static files as well. This can be utilized in several ways:
- providing image files for the auto-documentation
- providing logos and custom stylesheets for the UI customization
All custom files must be placed in one directory structure, the path to this
directory can be specified as the local_dir
argument.
from mutaprops import *
man = HttpMutaManager("Some manager", loop=loop,
local_dir="path/to/custom/dir")
The contents of the local_dir
will be served in the path /local
.
Custom style modification¶
To modify the UI look-and feel, one can produce a custom stylesheet and override
the stylesheet in mutaprops/web_ui/dist/base.css
.
This stylesheet must be called custom.css
and must be placed in the
local_dir
. Any additional asset files (logos, images etc.) must be placed
there as well.
UI self-documentation and help files¶
Each parameter is documented with its own docstring (ReST can be used for formatting).
On top of that, an additional help text can be displayed in the help window
(activated by the help link in the menu bar).
This text is specified as the help_doc
argument, the content must be a
string containing HTML code.
To translate a ReST text into suitable help_doc
,
the mutaprops.utils.rest_to_html()
can be used.
from mutaprops import *
from mutaprops.utils import rest_to_html
help = """
Help-less
---------
This help is of *NO USE* at all!
"""
man = HttpMutaManager("Some manager", loop=loop,
local_dir="path/to/custom/dir",
help_doc=rest_to_html(help))
Log forwarding¶
UI manager is capable of forwarding the logging
messages to the HTML UI.
In order to do so, a logger has to be registered with the manager using the
proxy_log
argument.
from mutaprops import *
import logging
logger = logging.getLogger(__name__)
man = HttpMutaManager("Some manager", loop=loop,
local_dir="path/to/custom/dir",
proxy_log=logger)
By default, all log messages are forwarded. The log level can be further
specified by the log_level
argument.
Manager clustering and chaining¶
Several running UI manager instances can be connected into one master manager. This feature can be used for example to create a joint UI console for several headless machines (e.g. RPi’s) that are used in a cluster, each running it’s mutaprops UI.
Furthermore, such clusters can be chained (a master manager connects to another, higher-level master manager). There is no fixed limit for such chains, however in practice the latencies will increase with each chain link.
# Running on machine 192.168.1.1
from mutaprops import *
man = HttpMutaManager("Some master manager")
dead_parrot = SomeDecoratedClass()
dead_parrot.muta_init("Parrot #1")
man.add_object(dead_parrot)
man.run(port=9000)
# Running on machine 192.168.1.2
from mutaprops import *
man = HttpMutaManager("Some slave manager",
master='http://192.168.1.1:9000')
dead_parrot = SomeDecoratedClass()
dead_parrot.muta_init("Parrot #2")
man.add_object(dead_parrot)
man.run(port=9000)
In the above example, the Parrot #2
will appear in the UI served at
http://192.168.1.1:9000
, alongside the Parrot #1
At the same time, it will be also accessible from it’s own UI served at
http://192.168.1.2:9000
. Any change on any of those UI’s will be reflected
on the other UI as well.
Registering and unregistering objects with UI manager¶
Once an object is instantiated from the muta-decorated class, there is nothing special going on with it until the mutaproperties are initialized and registered with the UI manager.
To initialize the mutaproperties,
the muta_init()
must be called.
During this initialization process, an ID (UI-visible name) is assigned to
a given object.
from mutaprops import *
dead_parrot = SomeDecoratedClass()
dead_parrot.muta_init("Parrot #1") # ID is assigined to the dead_parrot instance.
After mutaproperties are initialized, the object can be added to an existing
UI manager using add_object()
.
man = HttpMutaManager("Some master manager")
man.add_object(dead_parrot)
The whole process can be simplified by initializing the mutaproperties when adding the object to the manager
dead_parrot = SomeDecoratedClass()
man = HttpMutaManager("Some master manager")
man.add_object(dead_parrot, "Parrot #1") # Mutaproperties initialized while adding
Objects that were once added to the manager can be removed in the similar
fashion using the add_object()
.
man.remove_object(dead_parrot)
Running the manager¶
UI manager is basically just a server, and need to be run. In case of asyncio-based application, this is very simple, just as starting any sort of server.
# Continuing the example above
man.run(port=9000)
In case the rest of your application is not asyncio based and there are other tasks which has to be run alongside the user-initiated actions, it’s possible to run the UI manager in a separate thread.
# Continuing the example above
man.run_in_thread(port=9000)
This feature is not very well tested, and by definition opens all sort of synchronization problems which needs to be dealt with by the implementator. Use at your own risk!.
Run parameters¶
UI manager is built on the top of the aiohttp server, it’s therefore possible to use any parameters used in the aiohttp.run_app() method.
Using the UI¶
The UI itself is just a simple HTML5 application. Change of any property causes immediate change of the corresponding attribute of the underlying object.
Conversely, any change of underlying object’s state causes immediate update of the UI state.
There is some color coding to help making sense of those transitions:
- Orange code - value is being changed by the user
Orange label/background of the widget singalizes that the UI is currently being changed by the user and it’s state does not correspond with the underlying object’s state. The value on the label shows the value which is currently set on the object.
As soon as user stops changing the property value (widget looses it’s focus), the value is set to the underlying object and the orange code disappears.
- Blue code - value changed by the underlying object
Blue code signalizes user that the underlying object’s state has changed without user’s interaction. Blue label shows the last value before this change.
Blue label dissapears when the value is changed by the user.
- Azure code - value changed by another user session
- Since UI manager supports multiple parallel sessions (=multi-user), this mechanism signalizes the changes caused by another users. Azure label shows the last value before this change.
- Red code - value could not be updated
- Red background on the widget signalizes that the user change on the UI could not be set to the underlying object. Usually because the connection to the UI manager was lost etc.