Package grassyknoll :: Package frontend :: Module RestCollection
[hide private]

Source Code for Module grassyknoll.frontend.RestCollection

  1  #!/usr/bin/python  
  2  """ 
  3  prototype implemenation of a RESTful L{Collection}. 
  4   
  5  Built on L{EasyRest}. Much inconsistency follows. 
  6  """ 
  7   
  8  from grassyknoll.collection.Collection import * 
  9   
 10  import os.path 
 11  import glob 
 12   
 13  from grassyknoll.lib.meta import Spatch 
 14  from grassyknoll.lib import Norman  
 15  from grassyknoll.lib import strUniqueId 
 16   
 17  from werkzeug.templates import Template 
 18  import werkzeug.wrappers 
 19  import werkzeug.utils 
 20   
 21  from EasyRest import (RestApplication, RestRequest, RestResponse, RestView, 
 22                        exceptions, Rule) 
 23   
 24  from grassyknoll.serial import JsonSerial, PlainSerial 
 25   
 26   
27 -class Bunch(object):
28 - def __init__(self, **kwds):
29 self.__dict__.update(kwds)
30
31 -def url_for_id(id):
32 """return a suitable url for a Collection id""" 33 return '/'+werkzeug.utils.url_quote(id)
34
35 -def templatePath(name):
36 """return a full path for template name""" 37 return os.path.join(os.path.dirname(__file__), "templates/"+name)
38 39 40 _result_template=Template.from_file(templatePath('result.html')) 41 _result_template.default_context=_result_template.default_context.copy() 42 _result_template.default_context['url_for_id']=url_for_id 43
44 -class CollectionTemplate(Template):
45 """a L{Template} for outputting collection objects as HTML """ 46 default_context={ 47 'result_template':_result_template, 48 'metadata_template':Template.from_file(templatePath('metadata.html')), 49 'header_template':Template.from_file(templatePath('header.html')), 50 'footer_template':Template.from_file(templatePath('footer.html')), 51 'url_for_id':url_for_id 52 } 53 54 default_context.update(Template.default_context)
55
56 -class CollectionRequest(RestRequest):
57 """A Request object for L{CollectionApplication}""" 58 59 @werkzeug.wrappers.cached_property
60 - def fields(self):
61 """extract values from &fields= as a set""" 62 fields=self.args.get('fields') 63 if fields is not None: return tuple(fields.split(',')) 64 return None
65
66 -class CollectionApplication(RestApplication):
67 68 requestType=CollectionRequest 69 70 templates=Bunch() 71 templates.CollectionResult=CollectionTemplate.from_file(templatePath('CollectionResult.html')) 72 templates.CollectionResultSet=CollectionTemplate.from_file(templatePath('CollectionResultSet.html')) 73 templates.CollectionIds=CollectionTemplate.from_file(templatePath('CollectionIds.html')) 74 75
76 - def __init__(self, collection, model):
77 view=CollectionView(collection, model) 78 loadSpatch=Spatch(json=self.loadJson,) 79 #formdata=self.loadFormData) 80 81 dumpSpatch=Spatch(json=self.dumpJson, 82 html=self.dumpHtml, 83 plain=self.dumpPlain) 84 85 super(CollectionApplication, self).__init__(view, loadSpatch, dumpSpatch)
86 87 # XXX all this could use some genericization
88 - def loadJson(self, request):
89 """load python objects from JSON in request""" 90 assert isinstance(request, RestRequest) 91 return JsonSerial.loads(request.data)
92
93 - def dumpJson(self, request, obj):
94 """dump a JSON representation of obj to a response 95 96 @arg request: the current Request 97 @type request: L{CollectionRequest} 98 99 @arg obj: the object to dump 100 @type obj: object 101 102 @rtype: L{RestResponse} 103 """ 104 # XXX should we construct a fully-qualified URL here, or expect the client to do it? 105 if isinstance(obj, CollectionResult): 106 obj.url=url_for_id(obj.id) 107 elif isinstance(obj, CollectionResultSet): 108 for r in obj: r.url=url_for_id(r.id) 109 elif isinstance(obj, CollectionIds): 110 pass 111 else: 112 raise TypeError("can't dump %r"%type(obj)) 113 114 s=JsonSerial.dumps(obj.dump()) 115 response=RestResponse(s, mimetype='application/json') 116 response.content_length=len(s) 117 return response
118
119 - def loadFormData(self, request):
120 """load python objects from form data in request""" 121 raise NotImplementedError()
122
123 - def dumpHtml(self, request, obj):
124 """dump a HTML representation of obj to a response 125 126 @arg request: the current Request 127 @type request: L{CollectionRequest} 128 129 @arg obj: the object to dump 130 @type obj: object 131 132 @rtype: L{RestResponse} 133 """ 134 135 if isinstance(obj, CollectionIds): 136 s=self.templates.CollectionIds.render(ids=obj, metadata=obj.metadata, title=request.endpoint) 137 elif isinstance(obj, CollectionResult): 138 s=self.templates.CollectionResult.render(result=obj, title=obj.id) 139 elif isinstance(obj, CollectionResultSet): 140 s=self.templates.CollectionResultSet.render(result_set=obj, metadata=obj.metadata, title=request.endpoint) 141 else: 142 raise TypeError("can't dump %r"%type(obj)) 143 144 response=RestResponse(s, mimetype='text/html') 145 response.content_length=len(s) 146 return response
147
148 - def dumpPlain(self, request, obj):
149 """dump a plain-text representation of obj to a response 150 151 @arg request: the current Request 152 @type request: L{CollectionRequest} 153 154 @arg obj: the object to dump 155 @type obj: object 156 157 @rtype: L{RestResponse} 158 """ 159 if isinstance(obj, (CollectionResult, CollectionResultSet, CollectionIds)): 160 s=PlainSerial.dumps(obj.dump()) 161 response=RestResponse(s, mimetype='text/plain') 162 response.content_length=len(s) 163 return response 164 else: 165 raise TypeError("can't dump %r"%type(obj))
166
167 -class CollectionView(RestView):
168 """a RESTful wrapper around a L{Collection} 169 170 All methods return L{RestResponse}s 171 172 @ivar collection: the collection to wrap 173 @type colleciton: L{Collection} 174 175 @ivar model: convert raw uploads to L{CollectionDocument}s 176 @type model: L{Norman} 177 """ 178
179 - def rules(self):
180 return [ 181 ## basic Collection methods 182 Rule('/', methods=['GET'], endpoint='list'), 183 Rule('/<id>', methods=['GET'], endpoint='retrieve'), 184 Rule('/<id>', methods=['PUT'], endpoint='create'), 185 Rule('/<id>', methods=['DELETE'], endpoint='delete'), 186 187 ## operate on many by id 188 Rule('/__id__/<ids>', endpoint='retrieveMany', methods=['GET']), 189 Rule('/__id__/<ids>', endpoint='deleteMany', methods=['DELETE']), 190 191 ## extend, for creating several docs in one request 192 Rule('/__extend__/', endpoint='createMany', methods=['POST']), 193 # XXX GET? 194 195 ## append, for clients who don't create their own <id> 196 Rule('/__append__/', endpoint='append_new', methods=['POST']), 197 # XXX write a form 198 # Rule('/__append__/', endpoint='append_form', methods=['GET']), 199 200 ## map to collection.<name>Query(), returning CollectionResultSet 201 Rule('/__query__/<name>', endpoint='query', methods=['GET']), 202 # XXX add necessary introspection, see also comment in ClientCollection 203 # Rule('/__query__/', endpoint='query_list', methods=['GET'], 204 205 206 ## extra stuff 207 #Rule('/__stats__/', endpoint='stats', methods=['GET']), 208 #Rule('/__sync__/', endpoint='sync', methods=['POST']), 209 ]
210
211 - def __init__(self, collection, model):
212 assert isinstance(collection, Collection) 213 self.collection=collection 214 215 assert isinstance(model, Norman.Norman) 216 self.model=model
217 218 ## single items ##
219 - def list(self, request):
220 ids=self.collection.list() 221 return request.dump(ids)
222
223 - def retrieve(self, request, id):
224 # XXX canonicalize on sorted(fields) 225 results=self.collection.retrieve([id], request.fields) 226 if not results: 227 raise exceptions.NotFound() 228 else: 229 assert len(results) == 1 230 result=results[0] 231 return request.dump(result)
232
233 - def delete(self, request, id):
234 ids=self.collection.delete([id]) 235 # XXX the REST book says this s.b. 204 NO CONTENT, but that breaks 236 # ClientCollection (which needs to return CollectionUniqueIds). It also 237 # makes things ugly in browsers, which handle a 204 by doing... 238 # nothing. 239 return request.dump(ids)
240
241 - def create(self, request, id):
242 assert isinstance(request, RestRequest) 243 244 doc=self.__loadDoc(request) 245 assert isinstance(doc, CollectionDocument) 246 247 ## conflict b/w id in body & url 248 body_id=getattr(doc, 'id', None) 249 if (body_id and id) and body_id != id: 250 raise exceptions.BadRequest() 251 else: 252 doc.id=id 253 254 return self.__create(request, doc)
255
256 - def append_new(self, request):
257 doc=self.__loadDoc(request) 258 if not hasattr(doc, 'id'): doc.id = strUniqueId() 259 return self.__create(request, doc)
260 261 ## many items ##
262 - def retrieveMany(self, request, ids):
263 ids=ids.split(',') 264 265 # canonicalize 266 sorted_ids=sorted(ids) 267 if sorted_ids != ids: 268 # XXX hard-coded path, gross 269 # XXX maybe 301? 270 #import wingdbstub 271 url='/__id__/'+','.join(sorted_ids) 272 if request.environ['QUERY_STRING']: url=url+"?"+request.environ['QUERY_STRING'] 273 return werkzeug.utils.redirect(url, code=303) 274 275 # XXX canonicalize on sorted fields too 276 results=self.collection.retrieve(ids, request.fields) 277 return request.dump(results)
278
279 - def deleteMany(self, request, ids):
280 # XXX should we canonicalize here too? might matter for caches 281 ids=self.collection.delete(ids.split(',')) 282 # XXX see comment in delete 283 return request.dump(ids)
284
285 - def createMany(self, request):
286 obj=request.load() 287 if not isinstance(obj, list): raise exceptions.BadRequest() 288 289 try: 290 # XXX things will explode if doc.id isn't already set 291 docs=[CollectionDocument(fields=self.model(f)) for f in obj] 292 except Norman.NormanError: 293 raise exceptions.BadRequest() 294 295 ids=self.collection.create(docs) 296 response=request.dump(ids) 297 assert isinstance(response, RestResponse) 298 # XXX hard coded path, gross 299 response.headers.set("Location", '/__id__/'+','.join(sorted(ids))) 300 response.status_code=201 301 return response
302 303 ## query ##
304 - def query(self, request, name):
305 # XXX this could use introspection of collection's *Query 306 query_func=getattr(self.collection, name+"Query", None) 307 if query_func is None: raise exceptions.NotFound() 308 309 # XXX this doesn't do any type coercion on args 310 # XXX &fields= should be processed specially 311 kwargs=request.args.to_dict() 312 results=query_func(**kwargs) 313 return request.dump(results)
314 315 ## internal ##
316 - def __loadDoc(self, request):
317 obj=request.load() 318 319 try: 320 obj=self.model(obj) 321 except Norman.NormanError: 322 raise exceptions.BadRequest() 323 324 doc=CollectionDocument(fields=obj) 325 return doc
326
327 - def __create(self, request, doc):
328 assert isinstance(doc, CollectionDocument) 329 330 ids=self.collection.create([doc]) 331 response=request.dump(ids) 332 assert isinstance(response, RestResponse) 333 response.headers.set("Location", url_for_id(doc.id)) 334 response.status_code=201 335 return response 336 337 if __name__=='__main__': 338 import werkzeug.debug 339 import werkzeug.serving 340 from grassyknoll.backend.dictionary import DictCollection 341 342 collection=DictCollection.DictCollection() 343 try: 344 app = CollectionApplication(collection, Norman.IdemNorman()) 345 app = werkzeug.debug.DebuggedApplication(app, evalex=True) 346 werkzeug.serving.run_simple('localhost', 8080, app, 347 use_reloader=True, 348 extra_files=glob.glob(templatePath('*.html'))) 349 finally: 350 collection.close() 351