Mulib/Documentation

From Second Life Wiki
Jump to navigation Jump to search

Mulib Documentation

Mulib is a Python web application library. Mulib provides various functions and classes which implement WSGI applications for various Python types and MIME content types. The two main features of Mulib are object traversal over arbitrary Python types (such as dict, list, and string) using an extensible handler adapter registry, and content negotiation to arbitrary MIME content types (such as text/html and application/json) using the same extensible handler adapter registry.

Basics

To use Mulib in a wsgi environment, there are two different options. mulib.mu.Publisher(any_obj) is a class which takes any Python object as it's only argument. Instances of Publisher are WSGI applications which will traverse over the object graph rooted at that object. mulib.mu.delegate(env, start_response, any_obj, path_segments=None) is a function which takes a WSGI request, any Python object, and a tuple or list of path segments to traverse over the object, and handles the WSGI request. If path_segments is ommitted, the path in the WSGI environment PATH_INFO variable will be split on '/' and used for traversal.

Perhaps some examples will help to clarify. To create a WSGI application object which responds to two urls, 'hello' and 'foo', the following would be used:

Publisher example

from eventlet import api, wsgi
from mulib import mu

application = mu.Publisher({'hello': 'world', 'foo': 'bar'})

wsgi.server(api.tcp_listener(('', 8080)), application)

eventlet.wsgi is used as an example wsgi server, but any wsgi server may be used to serve this WSGI application. The application will respond to two urls: the url '/hello', to which it will reply 'world', and the url '/foo', to which it will reply 'bar'. This is because the handler registered for type dict knows show to turn url segments into accesses of dictionary keys. Note that the url '/foo/bar' would not be valid because the handler for type str does not allow child access since strings do not have conceptual children.

This is what is meant by object traversal; we start with a root Python object and look for a handler based on the object's type. We then call this handler, which may call other handlers if there are more path segments to consume, until we reach the leaf node.

Delegate Example

Another usage of Mulib would be inside an existing WSGI application. In this case, the mulib.mu.delegate function can be quite useful.

from eventlet import api, wsgi
from mulib import mu

def application(env, start_response):
    return mu.delegate(env, start_response, "hello, world", ())

wsgi.server(api.tcp_listener(('', 8080)), application)

This will result in an application which responds to ALL urls with "hello, world". This is because we explicitly pass () as the path segments, which means mulib will ignore PATH_INFO entirely and assume the string "hello, world" is the leaf node immediately.

Content Negotiation Example

Here is another more complicated example, which highlights the content negotiation feature of mulib:

from eventlet import api, wsgi
from mulib import mu

def application(env, start_response):
    return mu.delegate(env, start_response, {"hello": "world"}, ())

wsgi.server(api.tcp_listener(('', 8080)), application)

In this case, we delegate to a Python dictionary, but we still don't pass any path segments. This application will also respond to any URL, but the actual bytes which are sent in the response will be based on the result of content negotiation. content negotiation is a process where the HTTP client sends an Accept header containing a list of acceptable mime types along with weights indicating the browser's preferences. Mulib will look at the Python type of the object being traversed (in this case, a dict) and determine which handler to pull out of the adapter registry based on the mime type of the handler.

In the case of dict, mulib has several handlers registered for different mime types. application/json, text/html, and text/plain. If the browser specifies application/json as the content type, the result will be

{"hello": "world"}

. However, if the browser specifies text/html as the content type, the result will be

<html><body><dl><dt>hello</dt><dd>world</dd></dl></body></html>

.

Subclassing mu.Handler

Sometimes, it is nice to write Python classes which implement a REST interface. mulib.mu.Handler provides an easy way to do this. There are three things you need to know about when subclassing mu.Handler.

First, by default mu.Handler will allow traversal to any attributes of self which do not start with an underscore (_).

Second, once an instance of mu.Handler has been selected as the leaf node, the REQUEST_METHOD is examined to determine which method to call on the Handler instance. If the REQUEST_METHOD is GET, _handle_GET(env, start_response) will be called. If the REQUEST_METHOD is PUT, _handle_PUT(env, start_response) will be called, and so on.

Implementing REST Example


import os.path

from eventlet import api, wsgi

from mulib import mu


def relative(filename):
    return os.path.join(os.path.split(os.path.abspath(__file__))[0], filename)


class Baz(mu.Handler):
    def _handle_GET(self, env, start_response):
        start_response('200 OK', [('Content-type', 'text/plain')])
        return ["Baz!\n"]


class Bar(mu.Handler):
    index = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
	<head>
		<title>Url Hierarchy Example</title>
	</head>
	<body>
	   <a href="..">up</a>
	   <h1>Bar!</h1>
	   <a href="baz">baz</a>
	</body>
</html>"""
    baz = Baz()


class Foo(mu.Handler):
    def _handle_GET(self, env, start_response):
        start_response('200 OK', [('Content-type', 'text/plain')])
        return ["Foo!\n"]


class Root(mu.Handler):
    index = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
	<head>
		<title>Url Hierarchy Example</title>
	</head>
	<body>
	   <a href="foo">foo</a>
	   <a href="bar/">bar</a>
	</body>
</html>"""

    foo = Foo()
    bar = Bar()


wsgi.server(
    api.tcp_listener(('', 8080)), ## Socket to listen on
    mu.Publisher(Root()))  ## wsgi app to run

Customizing Handler traversal example

Finally, if you wish to customize traversal over your Handler subclass, override the _traverse(env, start_response, segments) method. Although the traverse method has a signature which does not exactly match the WSGI application signature, it's semantics are the same. If there are additional unhandled segments below what your Handler wants to handle, it's convenient to call the delegate function to dispatch the request to another handler.

"""This is a port of the web.py example from the homepage.

http://webpy.org/

Since mulib is now pure wsgi, it doesn't have nice request
and response abstractions. This example would be a lot nicer
if it used WebOb, a wsgi request/response abstraction.
"""

import cgi

from eventlet import api, wsgi

from mulib import mu, handlers


class HelloServer(handlers.Handler):
    def _traverse(self, env, start_response, segments):
        return mu.delegate(
            env, start_response,
            Hello('/'.join(segments)), ## This object will handle the request.
            ()) ## This is a tuple of the unhandled segments.
                ## We handled them all, so this is an empty tuple.


class Hello(handlers.Handler):
    def __init__(self, who):
        if not who:
            who = 'world'
        self.who = who

    def _handle_GET(self, env, start_response):
        times = cgi.parse_qs(env['QUERY_STRING']).get('times', [1])[0]

        start_response('200 OK', [('Content-type', 'text/plain')])
        for c in range(int(times)):
            yield 'Hello, %s!\n' % self.who


wsgi.server(
    api.tcp_listener(('', 8080)), ## Socket to listen on
    mu.Publisher(HelloServer()))  ## wsgi app to run

Handler Adapters

A handler adapter is a WSGI application callable which is associated with a Python type using the mu.set_handler(typ, callable, content_type='text/plain') function. Whenever mulib is traversing an object of type typ, or an object of type typ has been selected as the leaf node and content negotiation has selected the content type text/plain, the WSGI application callable will be called.

Handler Adapters Example

import os.path

from eventlet import api, wsgi

from mulib import mu

class MyClass(object):
    def __init__(self, name):
        self.name = name


def handle_myclass(env, start_response):
    instance, path = mu.get_current(env)
    if env['PATH_INFO'] != '/':
        return mu.not_found(env, start_response)

    start_response('200 OK', [('Content-type', 'text/plain')])
    return ["Hello, %s!" % instance.name]

mu.set_handler(MyClass, handle_myclass)

wsgi.server(
    api.tcp_listener(('', 8080)), ## Socket to listen on
    mu.Publisher(MyClass('World'))  ## wsgi app to run

Object Traversal Details

First, the environment variable 'mu.remaining_segments' is calculated by splitting the url segments from PATH_INFO. Then, a handler for the type of the mu environment variable 'mu.current_node' is selected from the mu.adapters.Handlers instance from the environment variable 'mu.handler_adapters' and called, until the wsgi environment variable 'mu.remanining_segments' is (), indicating that we have reached the leaf node. Once we have reached the leaf node, content negotiation is performed to actually render the response to the request.

Content Negotiation Details

Once the leaf node has been reached (indicated by setting the WSGI environment variable 'mu.remaining_segments' to ()), the Accept header is parsed and iterated. For each content type a handler is selected from 'mu.handler_adapters'. If a handler is available, that handler is the final WSGI application which will render the response.

Parsers

The mu.parsers WSGI environment variable should be set to a dictionary of MIME type to callable(str) function. If this environment variable is not set, the default parsers will be used (parsers for application/x-www-form-urlenpred and application/json). The function mu.parsed_body can be used to read the request body and parse it based on the Content-Type and the parsers dictionary.

The implementations of the handlers for dict and list use parsed_body to implement PUT, allowing dictionary and list values to be set to arbitrary objects.

Parsers Example

from mulib import mu
from eventlet import api, wsgi
root = {'hello':'Hello World', 'contacts': {}}
wsgi.server(api.tcp_listener(('', 5000)), mu.SiteMap(root))

With the above server running, execute the following curl commands in a shell:

curl -X PUT -H "Content-type: application/json" -d '{"Chuck" : { "Name": "Chuck", "Age":99, "Languages": ["Python", "C++"]}}' http://localhost:5000/contacts

Then, visiting the url http://localhost:5000/contacts will display the newly-uploaded data either in JSON or HTML formats.

Glossary

object traversal: The process of recursively selecting handlers based on the types of the objects in the object graph, starting at the root node and traversing over the path segments split from PATH_INFO.

content negotiation: The process of selecting a handler based on the Accept header given by the browser once object traversal has been completed.

leaf node: The object which was selected as the target of rendering for a WSGI request. content negotiation will be performed and the negotiated handler will be called to finalize and render the response.