Hello everyone!

Excuse me for raising this thread from the dead. It‘s Pyramid after all. 
You know, mummies, undead… :)

I tend to agree with Rocky but have no real experience with REST to back 
this. On the one hand, RESTful service can be seen as resource tree and 
traversal is the way of defining resource trees in Pyramid (BTW, I'm not 
the 
only<http://michael.merickel.org/projects/pyramid_auth_demo/object_security.html#why-is-this-interesting>
 person 
who thinks this way). On the other hand, this approach doesn‘t seem to be 
popular among extension writers. For instance, both cornice and 
pyramid_rest use URL dispatch. Please, correct me if I'm mistaken.

Last year I used traversal in an app with somewhat RESTish URI space where 
it was possible to get location-aware resources either in context of their 
parent resources' collections or top-level collections (e.g. 
/companies/:company_id/users/:user_id and /users/:user_id). I had List and 
Instance resource base classes and subclasses of these corresponding to 
each SA model class:

# resources.py
class Base(object):
    # …

    # URL generation stuff like mapping model resource classes to their 
traversal names etc.
    def url(self, *elements, **kw):
        if kw:
            return self.request.resource_url(self, *elements, query=kw)
        return self.request.resource_url(self, *elements)

    def resource_name(self, model_class):
        return self.resource_names[model_class]

    def model_class_url(self, model_class, *elements, **kw):
        return self.url(self.resource_name(model_class), *elements, **kw)

    def resource_class(self, resource_name):
        return self.resource_classes[resource_name]

    def model_instance_url(self, class_or_instance, id=None, *elements, 
**kw):
        if isinstance(class_or_instance, models.Base):
            return self.model_class_url(type(class_or_instance),
                                  class_or_instance.id or id,
                                  *elements, **kw)
        if isinstance(class_or_instance, type) and id is not None:
            return self.model_class_url(class_or_instance, id, *elements, 
**kw)

    # Useful in templates where you don‘t have model classes at hand
    def company_url(self, *elements, **kw):
         return self.model_class_url(model.Company, *elements, **kw)

    def user_url(self, *elements, **kw):
    #... 

class Model(Base):
    model_class = None

class List(Model):
    item_class = None
    parent_item_class = None
    _model_parent = None

    @property
    def model_parent(self):
        instance = self._model_parent
        if instance:
            self.request.dbsession.add(instance)
        return instance

    @model_parent.setter
    def model_parent(self, instance):
        self._model_parent = instance

    def __getitem__(self, key):
        item = self.item_class(self.request, key, self)
        return item

    @property
    def has_parent(self):
        return self.parent_item_class is not None


class Instance(Model):
    model_instance = None
    model_child_class = None

    def __init__(self, request, name, parent):
        super(Instance, self).__init__(request, name, parent)
        model_id = name
        model_instance = 
request.dbsession.query(self.model_class).get(model_id)
        if model_instance is None:
            raise KeyError(name)
        self.model_instance = model_instance

    def __getitem__(self, key):
        if (self.model_child_class and
            key == self.resource_name(self.model_child_class)):
            item_class = self.resource_class(key)
            item = item_class(self.request, key, self)
            item.model_parent = self.model_instance
            return item
        raise KeyError(key)

    @property
    def current(self):
        return self.request.context.__parent__ is self.__parent__

class CompanyInstance(Instance):
    model_class = models.Company
    model_child_class = models.User
    # …

class CompanyList(List):
    model_class = models.Company
    item_class = CompanyInstance
    # …

In addition to that I used a similar approach to share code between 
class-based views but the implementation was more complicated because of 
form handling:

# views.py
class BaseForm(object):
    # common form stuff

    schema_class = None

    @property
    def schema(self):
        return self.schema_class()

    form_class = None
    form_args = None

    @reify
    def form(self):
        form_class = self.form_class
        schema = self.schema
        form = form_class(schema, **self.form_args)
        form = self.update_form(form)
        return form

    def update_form(self, form):
        return form

    @property
    def model_class(self):
        return self.context.model_class

    @property
    def dbsession(self):
        return self.request.dbsession

class List(BaseForm):
    form_args = dict(method='GET', buttons=('search',))
    # defined in subclasses
    header_items = None
    item_template = None
    items_per_page = 10

    def __call__(self):
        # query using params from filter form and paginate result
    # …

class AppstructMixin(object):
    # we need to make a colander appstruct out of model instance to render 
the form
    def appstruct(self, schema, instance):
        return schemas.instance_to_appstruct(schema, instance)

class SaveInstance(BaseForm, AppstructMixin):
    form_args = dict(buttons=(_('Save'),))

    def save(self, instance, form):
        # update instance with form data if valid
        # redisplay form w/ errors otherwise

    def instance_url(self, instance):
        # URL for redirect
        # implemented in subclasses
        raise NotImplemented


class New(SaveInstance):
    def __call__(self):
        # …

class Edit(SaveInstance):
    def __call__(self):
        # …

class View(BaseForm, AppstructMixin):
    form_args = dict(buttons=(_('Edit'),))

    def __call__(self):
        # …

class InstanceMixin(object):
    def update_form(self, form):
        dbsession = self.dbsession
        # form widgets may need something from DB (e.g. for select options)
        # …
        return form

class CompanyList(List):
    schema_class = schemas.CompanyFilter
    form_class = forms.CompanyFilter
    header_items = [
        _("ID"),
        _("Name"),
        # …
    ]
    item_template = 'listing/company_item'

class CompanyMixin(InstanceMixin):
    schema_class = schemas.Company
    form_class = forms.Company

class CompanyNew(CompanyMixin, New):
    pass

class CompanyEdit(CompanyMixin, Edit):
    pass

class CompanyView(CompanyMixin, View):
    def appstruct(self, schema, instance):
        # specialized appstruct

# and so on…

I wasn't happy with this design because of parallel class hierarchies 
(model <-> resource <-> view). On the other hand, it was pretty 
straightforward and allowed handling special cases explicitly. For 
instance, exposing relationship attributes was mere branching in specific 
sub-class‘ __getitem__, while more general approach would require some 
extra machinery like in khufu_traversal. Plus, the code base was small 
enough to remain manageable. Of course, I would appreciate other 
suggestions here.

Also, I‘ve seen a 
blog<http://pieceofpy.com/blog/2011/08/01/pyramid-and-traversal-with-a-restful-interface/>
 about 
traversing SA instances directly and I share the author's concerns about 
this approach. On the other hand, Kotti does exactly this.

Anyway, does anyone else practice this kind of things for building REST 
APIs? If yes, what‘s your approach? 

Thanks!

On Tuesday, March 15, 2011 7:34:04 AM UTC+4, Rocky Burt wrote:
>
> I still think traversal-based resources lend themselves to RESTful 
> mappings better than route-based routines.
>
> GET /notes              # get a listting of all notes
> POST /notes             # create a new note
> GET /notes/firstnote    # get full information on the "firstnote" note
> PUT /notes/firstnote    # change the details of the "firstnote" note
> DELETE /notes/firstnote # delete the "firstnote" note
>
> In this scenario, we would have traversal resources like this...
>
> root                    # an instance of Root
> root/notes              # an instance of NotesContainer
> root/notes/firstnote    # an instance of Note
>
> And then we would have views such as (what follows is pseudo code)...
>
> @view_config(request_method='GET', context=NotesContainer)
> def notes_listing(request):
>     return {'notes': db.query(Note).all()}
>
> @view_config(request_method='POST', context=NotesContainer)
> def add_note(request):
>     note = Note(**somevals)
>     db.add(note)
>     db.flush()
>     return single_note(request, note.id)
>
> @view_config(request_method='GET', context=Note)
> def single_note(request, note_id=None):
>     if note_id is None:
>         note_id = request.context.__name__
>     return {'note': db.query(Note).filter_by(Note.id==note_id}
>
>
>

-- 
You received this message because you are subscribed to the Google Groups 
"pylons-devel" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to pylons-devel+unsubscr...@googlegroups.com.
To post to this group, send email to pylons-devel@googlegroups.com.
Visit this group at http://groups.google.com/group/pylons-devel?hl=en.
For more options, visit https://groups.google.com/groups/opt_out.


Reply via email to