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.