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.