Introduction
This document explains how to have more fine grain control over what gets encoded by PyAMF 0.5 and newer.
A simple example is that of a user object. This User class has username and password attributes as well a number of meta properties.
We are going to use the Google App Engine adapter for this example, but the ideas apply in all situations.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # models.py
from google.appengine.ext import db
import pyamf
class User(db.Model):
username = db.StringProperty()
password = db.StringProperty()
name = db.StringProperty()
dob = db.DateProperty()
pyamf.register_class(User, 'com.acme.app.User')
|
This class is used in a (theoretical) PyAMF application to represent User objects through a gateway:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import logging
from google.appengine.ext import db
from pyamf.remoting.gateway.wsgi import WSGIGateway
from models import User
class UserService(object):
def saveUser(self, user):
user.put()
def getUsers(self):
return User.all()
services = {
'user': UserService
}
gw = WSGIGateway(services, logger=logging)
|
Lets examine getUsers. Assuming that User.all() worked as expected a list of User objects would be returned to PyAMF for encoding which is the expected behaviour. What is not the desired behaviour is that the password attribute will be encoded with each User object, thereby handing all user passwords out to whomever desires them. Definitely not a good idea! The best solution would be to completely remove the password attribute from each User object as it is encoded.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | # models.py
from google.appengine.ext import db
import pyamf
class User(db.Model):
class __amf__:
exclude = ('password',)
username = db.StringProperty()
password = db.StringProperty()
name = db.StringProperty()
dob = db.DateProperty()
pyamf.register_class(User, 'com.acme.app.User')
|
Notice the class attribute __amf__. PyAMF looks for attributes on the object class that contain instructions on how to handle encoding and decoding instances.
The exclude property is a list of attributes that will be excluded from encoding and removed from the instance if it exists in decoding. Setting exclude = ('password',) gives us the desired effect of not sending the passwords of each User in the user.getUsersservice call.
The first argument of the UserService.saveUser method is a User object that has been decoded by PyAMF and applied to the service method. Some type checking might be in order here, because anything could be sent as the user payload.
1 2 3 4 5 | def saveUser(self, user):
if not isinstance(user, User):
raise TypeError('User expected')
db.save(user)
|
So now we can ensure that any call to user.put will be an instance (or subclass) of User. Since we plan to persist the User object, some validation is in order. If the correct attribute (_key) is sent to PyAMF, the GAE Adapter will load the instance from the datastore and it then applies the object attributes on top of this instance. This means that if the _key is known to a malicious hacker, a malformed client request could attempt to change the username property (or indeed change any other property on the model). The same thing could apply to the password field, but that has been solved by excluding it in the previous section.
Certainly something we don’t want to happen!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # models.py
from google.appengine.ext import db
import pyamf
class User(db.Model):
class __amf__:
exclude = ('password',)
readonly = ('username',)
username = db.StringProperty()
password = db.StringProperty()
name = db.StringProperty()
dob = db.DateProperty()
pyamf.register_class(User, 'com.acme.app.User')
|
Notice the new readonly property. This should be pretty self-explanatory but it is a list of attributes that are considered read-only when applying an attribute collection to an instance, just after they have been decoded.
Those are probably the two most used properties out of the way, so what else is there?
A static attribute is an attribute that is expected to be on every instance of a given class. A good example would be the primary key for an ORM object. It allows the AMF payloads to be reduced substantially (using AMF3 only).
1 2 3 4 5 6 7 | import pyamf
class Person(object):
class __amf__:
static = ('gender', 'dob')
pyamf.register_class(Person, 'com.acme.app.Person')
|
This means that the gender and dob attributes must be on every instance of the Person class. Decoding an instance that does not have these attributes will cause an AttributeError whilst decoding/encoding.
Flex provides two classes that are ‘bindable’ (ArrayCollection and ObjectProxy), making things easier for Flex developers (plenty of info/tutorial on the web!). A proxied attribute is purely AMF3 specific - when encoding an attribute, if it is labeled as proxy then a proxied version will be encoded. The reverse happens on decode, if a proxied version is encountered then the unproxied version is returned. This allows transparent proxying without having to disturb the underlying ‘raw’ attribute.
1 2 3 4 5 6 7 | import pyamf
class Person(object):
class __amf__:
proxy = ('address',)
pyamf.register_class(Person, 'com.acme.app.Person')
|
AMF provides the opportunity for the developer to customise the (de)serialisation of instances through the implementation of IExternalizable (once again, plenty of docs and tuts on the web). PyAMF makes no exception.
To implement IExternalizable:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import pyamf
class Person(object):
class __amf__:
external = True
def __writeamf__(self, output):
# Implement the encoding here
pass
def __readamf__(self, input):
# Implement the decoding here
pass
pyamf.register_class(Person, 'com.acme.app.Person')
|