Monday, June 11, 2012

JMX for Python

As soon as head-less Python projects crosses certain level of complexity, you start noticing that something is missing - something with buttons, text areas and everything else to visualize data structures, state of your project... something like UI :)

In world of Java you have JMX to address most of it... but what can we use in Python? While working under Scheduler [1], I found that the easiest way to implement "Python MX" is to use werkzeug [2].

So, what do we need to get it working? First of all, we need HTML template (here, we arranged output in two columns):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<table style="width: 100%"> <tbody>
{%- for row in details.entries %}
<tr>
<td>{{ row[0] }}</td>
<td>{{ row[1] }}</td>
</tr>
{%- endfor %}
</tbody> </table>
</body>
</html>

Next, we need to expose URL for the template:
from werkzeug.utils import cached_property, redirect
from werkzeug.wrappers import Response
from utils import render_template, expose, jinja_env
@expose('/')
@expose('/example_details/')
def example_details(request):
details = ExampleDetails(jinja_env.globals['mbean'])
return render_template('template_example.html', details=details)
def not_found(request):
return render_template('not_found.html')
# And implement details themselves:
class ExampleDetails(object):
def __init__(self, mbean):
self.mbean = mbean
@cached_property
def entries(self):
return [[self.mbean.property_1, self.mbean.property_2]]
view raw mx.views.py hosted with ❤ by GitHub
Now, all we left to do is - start the MX and set the Bean:
class MBeanExample(object):
def __init__(self):
self.property_1 = 'this is'
self.property_2 = 'example'
def start_mx(self):
""" import MX module (which has back-reference import to self) and start it """
from mx.mx import MX
self.mx = MX(self)
self.mx.start_mx_thread()
if __name__ == '__main__':
source = MBeanExample()
source.start_mx()
Where MX is:
from threading import Thread
from werkzeug.wrappers import Request
from werkzeug.wsgi import ClosingIterator, SharedDataMiddleware
from werkzeug.exceptions import HTTPException, NotFound
from werkzeug.serving import run_simple
from utils import STATIC_PATH, local, local_manager, url_map, jinja_env
import views
class MX(object):
def __init__(self, mbean):
local.application = self
self.mbean = mbean
jinja_env.globals['mbean'] = mbean
self.dispatch = SharedDataMiddleware(self.dispatch, {
'/static': STATIC_PATH
})
def dispatch(self, environ, start_response):
local.application = self
request = Request(environ)
local.url_adapter = adapter = url_map.bind_to_environ(environ)
try:
endpoint, values = adapter.match()
handler = getattr(views, endpoint)
response = handler(request, **values)
except NotFound as e:
response = views.not_found(request)
response.status_code = 404
except HTTPException as e:
response = e
return ClosingIterator(response(environ, start_response),
[local_manager.cleanup])
def __call__(self, environ, start_response):
return self.dispatch(environ, start_response)
def start_mx_thread(self, hostname = None, port = None):
"""Spawns a new HTTP server, residing on defined hostname and port
:param hostname: the default hostname the server should listen on.
:param port: the default port of the server.
"""
hostname = '0.0.0.0'
port = 5000
reloader = False # use_reloader: the default setting for the reloader.
debugger= False #
evalex=True # use_evalex: the default setting for the evalex flag of the debugger.
threaded=False # threaded: the default threading setting.
processes=1 # processes: the default number of processes to start.
reloader_interval = 1
static_files=None # static_files: optional dict of static files.
extra_files=None # extra_files: optional list of extra files to track for reloading.
ssl_context=None # ssl_context: optional SSL context for running server in HTTPS mode.
mx_thread = Thread(target=run_simple(hostname = hostname,
port = port,
application = self,
use_debugger = debugger,
use_evalex = evalex,
extra_files = extra_files,
use_reloader = reloader,
reloader_interval = reloader_interval,
threaded = threaded,
processes = processes,
static_files=static_files,
ssl_context=ssl_context))
mx_thread.daemon = True
mx_thread.start()
view raw mx.mx.py hosted with ❤ by GitHub
And utils.py looks like:

from werkzeug.utils import cached_property, redirect
from werkzeug.wrappers import Response
from utils import render_template, expose, jinja_env
@expose('/')
@expose('/example_details/')
def example_details(request):
details = ExampleDetails(jinja_env.globals['mbean'])
return render_template('template_example.html', details=details)
def not_found(request):
return render_template('not_found.html')
# And implement details themselves:
class ExampleDetails(object):
def __init__(self, mbean):
self.mbean = mbean
@cached_property
def entries(self):
return [[self.mbean.property_1, self.mbean.property_2]]
view raw mx.views.py hosted with ❤ by GitHub

As soon as coding is complete navigate in your browser to "localhost:5000":
And with some efforts it can turn to:


All things considered - Python MX is not a trivial hack, however not a paramount either. With great tools like Werkzeug our life is much easier.
For real-world example, please refer to Scheduler MX folder [3].

[1] Scheduler
[2] Werkzeug