Getting Started


A simple example


Just to get started, this is an example of taking an XML representation of an Address that might be returned from a GET request to an external REST api.

<Address id="2">
  <number>22</number>
  <street>Acacia Avenue</street>
  <city>Maiden</city>
  <country>England</country>
  <postcode>IM6 66B</postcode>
</Address>


class Address(xml_models.Model):
  id=xml_models.IntField(xpath="/Address/@id")
  number = xml_models.IntField(xpath="/Address/number")
  street = xml_models.CharField(xpath="/Address/street")
  city = xml_models.CharField(xpath="/Address/city")
  country = xml_models.CharField(xpath="/Address/country")
  postcode = xml_models.CharField(xpath="/Address/postcode")

This example would be used as follows:-

>>> print "address is %s, %s" % (address.number, address.street)
"22, Acacia Avenue"


XML Mapping options



The available field mappings are as follows

  • CharField(xpath="...", default="...") -- returns string data
  • IntField(xpath="...", default="...") -- returns integers
  • DateField(xpath="...", default="...", date_format="%Y-%m-%dT%H:%M:%S") -- returns a date from using the supplied date_format mask
  • FloatField(xpath="...", default="...") -- returns a floating point number
  • BoolField(xpath="...", default="...") -- returns a boolean
  • Collection(fieldtype, order_by=None, xpath="...", default="...") -- returns a collection of either one of the above types, or an xml_model.Model subclass

The first five fields are fairly self explanatory. The sixth field is where it gets interesting. This is what allows you to map collections of nested entities, such as:-

<Person id="112">
  <firstName>Chris</firstName>
  <lastName>Tarttelin</lastName>
  <occupation>Code Geek</occupation>
  <website>http://www.pyruby.com</website>
  <contact-info>
    <contact type="telephone">
      <info>(555) 555-5555</info>
      <description>Cell phone, but no calls during work hours</description>
    </contact>
    <contact type="email">
      <info>me@here.net</info>
      <description>Where possible, contact me by email</description>
    </contact>
    <contact type="telephone">
      <info>1-800-555-5555</info>
      <description>Toll free work number for during office hours.</description>
    </contact>
  </contact-info>
</Person>


This can be mapped using a Person and a ContactInfo model:-

class Person(xml_models.Model):
  id = IntField(xpath="/Person/@id")
  firstName = CharField(xpath="/Person/firstName")
  lastName = CharField(xpath="/Person/lastName")
  contacts = Collection(ContactInfo, order_by="contact_type", xpath="/Person/contact-info/contact")

class ContactInfo(xml_models.Model):
  contact_type = CharField(xpath="/contact/@type")
  info = CharField(xpath="/contact/info")
  description = CharField(xpath="/contact/description", default="No description supplied")


This leads to the usage of a person as :-

>>> person.contacts[0].info
me@here.com

Querying the REST api



An external REST api will present a limited number of options for querying data. Because the different options do not have to follow any specific convention, the model must define what finders are available and what parameters they accept. This still attempts to follow a Django-esque approach

class Person(xml_models.Model:
    ...
    finders = { (firstName, lastName): "http://person/firstName/%s/lastName/%s",
                (id,): "http://person/%s"}


The above defines two query options. The following code exercises these options

>>> people = Person.objects.filter(firstName='Chris', lastName='Tarttelin')
>>> people.count()
1
>>> person = Person.objects.get(id=123)
>>> person.firstName
Chris


When using a filter, a collection of 0 or more results are expected. The expectation is that the results are within a single enclosing tag, such as:-

<addresses>
  <address> ...</address>
  <address>...</address>
</addresses>


This would allow you to iterate over the results for models whose root tag is the
element. This pattern works for us at the moment, but we plan to support custom approaches in the future. The parser streams the results as they are pulled from the REST call so as to avoid upfront loading and potential memory issues.

Support for Namespaces



There is a primitive level of namespace support. By adding a namespace attribute to your model, all the xpaths will use this namespace, which allows the following to work

<address xmlns="http://www.pyruby.com">
  <houseNumber>999</houseNumber>
  <street>Letsbe Avenue</street>
  <city>London</city>
</address>


class Address(xml_models.Model):
  namespace = "http://www.pyruby.com"
  houseNumber = IntField(xpath="/address/houseNumber")
  street = CharField(xpath="/address/street")
  city = CharField(xpath="/address/city")


The namespace attribute assumes that all elements are within the same namespace. So far, nothing more complex has occurred, so we have not done anything extra. Because we version our REST apis, there is little benefit in using namespaces, so we tend not to.

Validation



In some applications, we only want to deal with a subset of the records returned because some are not valid in our circumstance. We have on load validation baked in, so when the model is constructed, it will be validated and an exception will be raised if validation fails.

def validate_on_load(self):
        if not self.street:
            raise XmlValidationError("An address without a street cannot be processed")


When iterating over a collection of addresses, if one doesn't have a street, an exception is thrown. This can then be caught and handled as appropriate.

Writing to the REST api



The rest_client.py file contains a rest client that supports PUTting and POSTing to a REST api. The xml_model does not provide anything to assist in this at this point. We haven't yet decided how best to handle PUTs etc, but the current favorite approach is to use a template to construct the XML, and then use the rest client to dispatch it. We will be creating patterns for this in the near future.