Accessing collections and elements

To introduce txyoga, we’re going to start with a simple example of a bunch of employees who work at a startup (because blogs with articles are just cliché).

Example code

# -*- mode: python; coding: utf-8 -*-
from twisted.web.resource import IResource

from txyoga import Collection, Element


class Company(Collection):
    """
    A company.
    """
    exposedElementAttributes = "name",



class Employee(Element):
    """
    An employee at a company.
    """
    exposedAttributes = "name", "title"

    def __init__(self, name, title, salary):
        self.name = name
        self.title = title
        self.salary = salary



startup = Company()

startup.add(Employee(u"lvh", u"CEO", 10000))
startup.add(Employee(u"asook", u"Junior intern", 1))

resource = IResource(startup)

First, the code defines a collection class called Company. Collections are containers for elements. The collection class has an attribute called exposedElementAttributes. These are the attribute the collection will expose about its elements. In this case, we want to show the names of our employees when the collection is queried. Obviously, the elements should have such a name attribute.

Note the comma at the end: the attribute is an iterable of the collection names (in this case, a tuple with one element). If you left it out, txyoga would just see a string, which is also an iterable of strings, but it’s quite likely that you don’t want to expose the attributes 'n', 'a', 'm', 'e'...

Once the collection is defined, the module defines an element class to go in the collection. In this example, that’s Employee. Its instances go in instances of the previously defined Company collection. The element class has an attribute called exposedAttributes. These are the attributes returned when the element itself is requested. Our fictuous employees tell people their names and are quite proud of their titles, but they’re shy when it comes to their salary.

Two sample employees are created and added to the collection, so there’s some data to play with when you try this code out.

Finally, it builds a resource from the collection, which allows it to be served. Resources are the things that Twisted serves – they’re roughly equivalent to the concept of a web page or a view. Twisted formally specifies what a resource is in an interface called IResource. Although the collection isn’t a resource itself, it can be turned into one, in a process called “adaptation”. That wraps the collection with an object (called the adapter) which behaves like a resource. That allows for a clean separation of concerns: being a collection on one side, and serving that collection on the other.

Both Collections and Elements are adaptable to resources. This means they’re easy to integrate with existing Twisted Web object hierarchies.

Trying it out

You can access the entire collection by sending an HTTP GET request to the root resource. txyoga uses JSON as the default serialization format. The examples use the json and httplib [1] modules from the standard library.

In the interest of readability, the examples use some helpers to make requests. These will build the appropriate URL and make the HTTP request. JSON decoding is done manually, so the response headers can still be verified. If you want to use these yourself, they are available in doc/tutorial/util.py.

# -*- coding: utf-8 -*-
"""
Utilties for making HTTP requests to txyoga tutorial examples.
"""
import functools
import httplib
import urllib


def buildPath(*parts):
    return "/" + "/".join(urllib.quote(part) for part in parts)


class Example(object):
    """
    A txyoga tutorial example.
    """
    def __init__(self, exampleName, host="localhost:8080"):
        self._makeConnection = functools.partial(httplib.HTTPConnection, host)
        self._buildPath = functools.partial(buildPath, exampleName + ".rpy")


    def _makeRequest(self, method, body, headers, *parts):
        """
        Makes an HTTP request to the tutorial example.
        """
        path = self._buildPath(*parts)
        connection = self._makeConnection()
        connection.request(method, path, body, headers)
        return connection.getresponse()


    def get(self, *parts):
        """
        Gets a particular collection or element.
        """
        return self._makeRequest("GET", "", {"Accept": "application/json"}, *parts)

        
    def delete(self, *parts):
        """
        Deletes a particular element.
        """
        return self._makeRequest("DELETE", "", {}, *parts)


    def put(self, body, headers, *parts):
        """
        Puts a particular element in a collection.
        """
        return self._makeRequest("PUT", body, headers, *parts)

Although it has other methods, we’ll only be using get in this example.

The buildPath function creates a URL path, roughly as you’d expect:

>>> buildPath("test.rpy")
'/test.rpy'
>>> buildPath("test.rpy", "lvh", "minions")
'/test.rpy/lvh/minions'

Create an Example object for this tutorial example:

>>> example = Example("accessing")

Accessing the collection

>>> response = example.get()
>>> json.load(response)
{u'prev': None, u'results': [{u'name': u'lvh'}, {u'name': u'asook'}], u'next': None}

The important key here is results. As you can see, has two entries, one for every employee. Each entry is a dictionary, containing the name of that employee. This is because Company.exposedElementAttributes only has name in it.

The two remaining keys, prev and next, are there for supporting pagination. In this case, they’re both None, indicating that there is neither a previous nor a next page. A later tutorial example will demonstrate how paginating collections works.

txyoga is being a good HTTP citizen behind the scenes, telling you the date, the web server serving the request, and content length and type behind the scenes.

>>> headers = response.getheaders()
>>> headerKeys = [k for k, v in headers]
>>> expectedKeys = ["date", "content-type", "transfer-encoding", "server"]
>>> assert all(k in headerKeys for k in expectedKeys)

txyoga will never return responses with missing content types. A later tutorial example on content type support in txyoga will elaborate on this.

>>> next(v for k, v in headers if k == "content-type")
'application/json'

Admittedly, it really is Twisted Web telling you it’s serving the request and the time it served it, not txyoga, but it’s still nice:

>>> serverName = next(v for k, v in headers if k == "server")
>>> assert "TwistedWeb" in serverName

Accessing an element

Let’s look at this lvh employee from up close next.

>>> response = example.get("lvh")
>>> json.load(response)
{u'name': u'lvh', u'title': u'CEO'}

As expected, you get a dictionary back with the name and title attributes of the employee, because those are the keys in Employee.exposedAttributes.

Although exposedElementAttributes and exposedAttributes are typically defined on the class they are looked up on the instance for each object. You could have a particular company that does expose the titles of its employees when you query it, or a particular employee that will divulge his salary.

As before, txyoga will serve the content type correctly:

>>> next(v for k, v in response.getheaders() if k == "content-type")
'application/json'

Review

In this example, we’ve:

  1. shown what basic txyoga code looks like
  2. shown how the tutorial example helpers work
  3. accessed collections and their elements over HTTP
[1]The examples use httplib and not urllib or urllib2, because it’s less of an abstraction over HTTP. It supports for HTTP PUT and DELETE, which are obviously important. Although urllib2 can be hacked at so that it does support those verbs, it’s not very pretty. urllib itself is still used because it provides quote.