1
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
29 self.__dict__.update(kwds)
30
32 """return a suitable url for a Collection id"""
33 return '/'+werkzeug.utils.url_quote(id)
34
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
55
57 """A Request object for L{CollectionApplication}"""
58
59 @werkzeug.wrappers.cached_property
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
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
86
87
89 """load python objects from JSON in request"""
90 assert isinstance(request, RestRequest)
91 return JsonSerial.loads(request.data)
92
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
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
122
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
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
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
180 return [
181
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
188 Rule('/__id__/<ids>', endpoint='retrieveMany', methods=['GET']),
189 Rule('/__id__/<ids>', endpoint='deleteMany', methods=['DELETE']),
190
191
192 Rule('/__extend__/', endpoint='createMany', methods=['POST']),
193
194
195
196 Rule('/__append__/', endpoint='append_new', methods=['POST']),
197
198
199
200
201 Rule('/__query__/<name>', endpoint='query', methods=['GET']),
202
203
204
205
206
207
208
209 ]
210
217
218
219 - def list(self, request):
222
224
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):
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
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
257 doc=self.__loadDoc(request)
258 if not hasattr(doc, 'id'): doc.id = strUniqueId()
259 return self.__create(request, doc)
260
261
263 ids=ids.split(',')
264
265
266 sorted_ids=sorted(ids)
267 if sorted_ids != ids:
268
269
270
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
276 results=self.collection.retrieve(ids, request.fields)
277 return request.dump(results)
278
284
286 obj=request.load()
287 if not isinstance(obj, list): raise exceptions.BadRequest()
288
289 try:
290
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
299 response.headers.set("Location", '/__id__/'+','.join(sorted(ids)))
300 response.status_code=201
301 return response
302
303
304 - def query(self, request, name):
305
306 query_func=getattr(self.collection, name+"Query", None)
307 if query_func is None: raise exceptions.NotFound()
308
309
310
311 kwargs=request.args.to_dict()
312 results=query_func(**kwargs)
313 return request.dump(results)
314
315
326
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