Welcome, guest | Sign In | My Account | Store | Cart

A recipe to represent a Directed Graph between different Entities (models), such that different connection types are supported. Allows for easy querying on connections. For example a social graph with Users and Images as entities; the connections are Follow and Like

Python, 70 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
from google.appengine.ext import db
from google.appengine.ext.db import polymodel
_connection_model_superclass = polymodel.PolyModel
class ConnectionModelMetaclass(type(_connection_model_superclass)):
    def __new__(cls, name, bases, dct):
        myname = name.replace('ConnectionModel','').lower()
        if myname:
            #this is not the baseclass
            to_collection_name = 'myto_%s_connections' % myname #or any other naming scheme you like
            from_collection_name = 'myfrom_%s_connections' % myname #or any other naming scheme you like
            myto = 'myto_%s'%myname
            myfrom = 'myfrom_%s'%myname
            dct[myto] = db.ReferenceProperty(collection_name = to_collection_name)
            dct[myfrom] = db.ReferenceProperty(collection_name = from_collection_name)
            if 'put' in dct:
                myput = dct['put']
            else:
                myput = None

            def put(self):
                setattr(self, myto, self.myto)
                setattr(self, myfrom, self.myfrom)
                self._validate_connected_types()
                if myput is not None:
                    myput(self)
                else:
                    MyClass = eval(name)
                    super(MyClass, self).put()
            dct['put'] = put
                
        return super(ConnectionModelMetaclass, cls).__new__(cls, name, bases, dct)
        
class ConnectionModel(_connection_model_superclass):
    __metaclass__ = ConnectionModelMetaclass
    ALLOWED_CONNECTIONS = {}#empty dict means anything goes. dict if of kind tuple->tuple
    timestamp = db.DateTimeProperty(auto_now = True)
    myto = db.ReferenceProperty(collection_name = 'myto_connections')
    myfrom = db.ReferenceProperty(collection_name = 'myfrom_connections')
    connection_index = db.StringProperty()#for strict sorting and paging of connections
    def _validate_connected_types(self):
        if None in (self.myfrom, self.myto):
            raise AttributeError
        if not self._check_connection():
            raise AttributeError(\
                'Connection %s --> %s is not allowed for class %s',
                self.myfrom.__class__.__name__,
                self.myto.__class__.__name__,
                self.__class__)
    
    def _check_connection(self):
        if len(self.ALLOWED_CONNECTIONS) == 0:
            return True
        for froms, tos in self.ALLOWED_CONNECTIONS.iteritems():
            if isinstance(self.myfrom, froms):
                if isinstance(self.myto, tos):
                    return True
        return False
        
    def put(self):
        if not self.connection_index:
            self.connection_index = '%s|%s|%s' % \
                    (self.timestamp, self.myfrom.key().name(),\
                        self.myto.key().name())
        super(ConnectionModel, self).put()

class LikeConnectionModel(ConnectionModel):
    ALLOWED_CONNECTIONS = {UserModel : ImageModel}
    
class FollowConnectionModel(ConnectionModel):
    ALLOWED_CONNECTIONS = {UserModel : (UserModel, ImageModel) }#users can follow users and (what the heck) follow images
     

This structure is a scalable(i hope) way of representing a graph with many connection types on GAE

For instance: user = UserModel.get(some_key) #get the Follow connections of the user query = user.myfrom_follow_connections #get any connection in which the user is the "from" query = user.myfrom_connections

For me the advantage of this approach lies in the polymorphism and extensibility. This is preferable than having each entity hold lists of keys for other entities. For instance creating a CommentConnectionModel is a 2-line class instead of adding a ListProperty on the UserModel. If we had for instance:

class CategoryConnectionModel(ConnectionModel):
    pass
class SubCategory1ConnectionModel(ConnectionModel):
    pass
class SubCategory2ConnectionModel(ConnectionModel):
    pass

we could query a user for each of the following:

user.myfrom_subcategory1_connections # return subcategory1 connection
user.myfrom_subcategory2_connections # return subcategory2 connections
user.myfrom_category_connections # return any (nonstrict)subclasses of CategoryConnectionModel

the ALLOWED_CONNECTIONS class property allows you to enforce that only connections of logical types can be created, and hopefully reduce/prevent some bugs.

Another note: the reason for this code in the metaclass

dct[myto] = db.ReferenceProperty(collection_name = to_collection_name)

is that i wanted to be able to use the reverse reference set that comes with ReferenceProperty for any node in the connection class inheritance tree. it might be a bit wasteful but imo it is worth it since the cost is not very high. (2 additional properties per inheritance depth)

Hope this is of use to anyone.

Reading:

https://developers.google.com/appengine/docs/python/datastore/polymodelclass https://developers.google.com/appengine/docs/python/datastore/typesandpropertyclasses#ReferenceProperty

Created by Simeon Shpiz on Sun, 29 Jul 2012 (MIT)
Python recipes (4591)
Simeon Shpiz's recipes (2)

Required Modules

  • (none specified)

Other Information and Tasks