← Back to team overview

lazr-users team mailing list archive

Re: Using lLazr.restful with scoped collections

 

On Jun 22, 2010, at 1:57 PM, Edward F. Long, Jr. wrote:

> Hello:
> 
> We are trying to implement a URI scheme that looks like this:
> 
> / person / uuid / music / uuid /
> 
> Where each person entry has a different collection of Music entries
> 
> For purposes of discussion, you can assume that no Person can have
> the same Music that another person has (In lazr terms, Music is a scoped
> collection of a specific Person entry, and Music is not a first level resource.
> 
> (We dont want to make everything first or top level, since there is an explicit
> this belongs to that relationship that we'd like to convey in the ReST URIs)
> 
> In order for the URI generation / traversal to work, we need to implement ILocation
> and provide __parent__  which is easy enough for Entry classes, but for collections
> it seems harder then it needs to be.
> 
> We'd like a collection of entries to just be a standard python list of <Entry> objects. 
> but since a python list knows nothing of __parent__, we've been painstakingly adding
> custom classes that extend from list and take a parent arg in the constructor that is returned
> by the __parent__ property.   We also ran into a minor issue with the handleCustomGet method
> of the CustomOperationResourceMixin class of lazr.restful (although handleCustomPost might
> also need this change)   to handle scoped collections correctly.  (see below for unified diff)  
> but we're not sure if the problem we encountered is a lazr bug, or a problem in how we're using lazr.
> 
> It feels like lazr.restful should be able to derive the parents of collections if we define our
> interfaces correctly and our ideal goal is to use sqlalchemy ORM models as Entry and
> Collection classes and expose them by making *very* minor changes to the models
> (adding implements(X), and the root resource's __parent__)
> 
> The only thing standing in the way of this goal is the parent / child issue
> 
> making a custom class that inherits from a standard list is a workable but less ideal solution and
> it adds some cruft in our sqlalchemy models that we'd rather not have to have if it doesnt need to 
> be there.
> 
> OR: does it need to be there to perform both sides of the object / uri mapping
>   ie:  Traversal (URI to object)  and URI Generation (object to URI)

It is needed for URI generation, even when the object has not been obtained via traversal.

> Here's a non-sqlalchemy example of what we'd like to accomplish
> 
> interface.py
>  1:  class IHasGet(Interface):
>  2:     def get(): pass
>  3:  
>  4:  class IMusic(IHasGet):
>  5:     export_as_webservice_entry()
>  6:     exported(text(title=u'song title')) 
>  7:
>  8: class IMusicCollection(IHasGet):
>  9:     export_as_webservice_collection(IMusic)
> 10:
> 11:    @collection_default_content() 
> 12:     def find(): pass
> 13:
> 14:  class IPerson(IHasGet):
> 15:     export_as_webservice_entry()
> 16:     music = exported(CollectionField(title=u'my music', 
> 17:                                    value_type=Reference(schema=IMusic)))
> 18:
> 19:  class IPersonCollection(IHasGet):
> 20:     export_as_webservice_collection(IPerson)
> 21:
> 22:     @collection_default_content() 
> 23:     def find(): pass
> 
> In our entry classes, we'd like to have something like this
> 
> resource.py
>  1: class Music(object):
>  2:     implements(IMusic, ILocation)
>  3:    @property
>  4:     def song_title(self):  
>  5:          return "....."
>  6:
>  7:     @property
>  8:     def __parent__(self): 
>  9:          return self.the_person_i_belong_to
> 10:
> 11: class Person(object):
> 12:     implements(IPerson, ILocation)
> 13:    @property
> 14:    def __parent__(self):
> 15:          return getUtility(IServiceRootResource)
> 16:
> 17:     @property
> 18:     def music(self):
> 19:           """ return a collection of music """
> 20:           return [  Music(), Music(), Music(), ... etc ]

In addition to implementing ILocation as you are mentioning (or having a custom absolute URL component), this needs to return an object that provides IMusicCollection.  If we ignore the ILocation concern (and I'll describe a way to do so below, though it won't be what you suggest), then it at least needs to be directly provided.  That won't work on a list...

>>> from zope.interface import Interface, directlyProvides
>>> class IFooCollection(Interface): pass
... 
>>> coll = []
>>> directlyProvides(coll, IFooCollection)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/gary/launchpad/lp-sourcedeps/eggs/zope.interface-3.5.2-py2.6-linux-x86_64.egg/zope/interface/declarations.py", line 861, in directlyProvides
    object.__provides__ = Provides(cls, *interfaces)
AttributeError: 'list' object has no attribute '__provides__'

...but it will on a subclass (that doesn't define a __slots__ or allows __provides__ to be set).

>>> class Collection(list): pass
... 
>>> coll2 = Collection()
>>> directlyProvides(coll2, IFooCollection)
>>> IFooCollection.providedBy(coll2)
True

Then your example could be written as follows:

@property
def music(self):
    res = Collection([Music(), ...])
    directlyProvides(res, IMusicCollection)
    return res

> 21: 
> 22: class PersonCollection(object):
> 23:     implements(IPersonCollection, IHasGet, ILocation)
> 24:     provides(IPersonCollection)
> 25:     __name__ = 'person'
> 26:   
> 27:    @property
> 28:    def __parent__(self):
> 29:          return getUtility(IServiceRootResource)
> 30:   
> 31:    @property
> 32:    def find(self):
> 33:          return [ Person(), Person(), Person(), ... ]
> 34:
> 35:    @property
> 36:    def get(self, name):
> 37:         (code to return a single Person instance)
> 
> The collection returned on line 20 of resource.py is a standard python list of Music objects
> 
> / person
>    knows its parent, since its explicitly stated in the definition of a PersonCollection
>    (resource.py line 27-29)

You aren't suggesting a change here, as I understand it.

> 
> / person / uuid
>    could this derive its parent from the interface, since we have 1 (and only 1) interface that
>    exports a collection of objects that each implement the IPerson interface
>    (interface.py line 20)  

Yes--not just only one interface, but it is a top-level collection, so the parent may be only one object in the whole system.

Your sample code doesn't set __name__, which is also necessary.

But yes, we could derive __parent__.  We could automatically set up an ILocation adapter especially for interfaces that come from top-level collections, if the code tells us with another interface annotation how to figure out where the __name__ should come from.  My drive to do this is low, but I see where you are coming from, and it wouldn't be that hard to accomplish, I suspect.

> / person / uuid / music
>    the parent of this is an entry that implements IPerson (/ person / uuid) and we know it
>    exports a 'music' property which is defined as a collection of Music
> 
>    could lazr derive its parent from the interface since its parent is  / person / uuid
> 
> / person / uuid / music / uuid
>    similar to / person / uuid, could we derive its parent from the interface, since we have
>    1 (and only 1) interface that exports a collection of Music objects, and we know that
>    its parent is an Entry that implements the IPerson interface?

No to both of the above, I believe.

The interfaces says that the parent of music collection is a person, but not *which* person.  The parent of a Music object is a Music Collection, but which one (that name is 'music,' but transitively, again, what's the __name__ of the person?

There are two possible mechanisms to not have these __name__ and __parent__ attributes pollute your SQLAlchemy model code.  They might or might not work for you.

First, you should be able to define an ILocation adapter for your various interfaces.  Here's an example:

from zope.interface import implements
from zope.component import adapts

class MusicCollectionLocation:
    implements(ILocation)
    adapts(IMusicCollection)
    def __init__(self, music):
        self._music = music

    __name__ = 'music'

    @property
    def __parent__(self):
        # Code to return the pertinent person who is the "parent" of the music object

Then you put this in the zcml to register it:

<adapter factory=".your_module_here.MusicCollectionLocation" />

If you are familiar with grok spelling, that works too.



Second, you should be able to define your own IAbsoluteURL implementation.  I won't get into that unless you ask.  That's more work, and I don't see why you'd need to do it yet.



> 
> 
> 
> (Unified diff of _resource.py)
> 
> --- lazr.restful-0.9.25/src/lazr/restful/_resource.py 
> +++ lazr.restful-0.9.25/src/lazr/restful/_resource.py
> @@ -682,0 +682,7 @@
> +        context = self.context
> +        if isinstance(context, ScopedCollection):

I'm not going to dig in right now to see if this is the right thing to do, though it looks reasonable.  Leonard, the lazr.restful expert will return in a few days, and he can weigh in.  However, if this is the right thing to do, we would spell that line above as ``if IScopedCollection.providedBy(context):``.

Once Leonard approves the idea, it would land faster if you could contribute a test, FWIW, but that's not necessary: we'll appreciate the bug report and fix.

Gary

> +            """Scoped collections dont expose the collection that is scoped
> +               directly, instead, you can find this by calling the scoped
> +               collection's 'collection' property"""
> +            context = self.context.collection
> +
> @@ -683,1 +690,1 @@
> -            operation = getMultiAdapter((self.context, self.request),
> +            operation = getMultiAdapter((context, self.request),
> 
> 
> We'd certainly welcome any feedback / advice / suggestions that anyone may have.
> 
> 
> 
> Edward F. Long, Jr.
> Web Developer
> AWeber Communications
> x748
> 
> Programmer: (n)
>   1: a multi-cellular organism that can convert caffeine into computer code (see also: geek)
> 
> 
> _______________________________________________
> Mailing list: https://launchpad.net/~lazr-users
> Post to     : lazr-users@xxxxxxxxxxxxxxxxxxx
> Unsubscribe : https://launchpad.net/~lazr-users
> More help   : https://help.launchpad.net/ListHelp




References