-
Notifications
You must be signed in to change notification settings - Fork 0
/
intgutil.py
executable file
·411 lines (359 loc) · 15.1 KB
/
intgutil.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
''' Json-Rest-handlin' integration helper.
This module offers a JSON+REST-handling integration class meant to be used with
Google App Engine (hooked into a webapp.RequestHandler subclass); it can be
hooked up by simply passing an object h with attributes h.request and
h.response that are duck-like those of webapp.RequestHandler.
On hookup, the integration-helper class overrides the get/set/put/delete
methods of the object hooking up to it so that they respond appropriately to
REST requests (as documented in json_rest.txt) based on registrations performed
in restutil, parsing and formatting JSON payloads based on jsonutil.
IOW, this helper integrates functionality found in other modules of the
gae-json-rest package:
parsutil
restutil
jsonutil
"putting it all together" into a highly-reusable (but still modestly
customizable) REST-style, JSON-transport server web-app for GAE.
TODO: decide what arguments/parameters are passed to various kinds of
methods being called, and implement that decision; add MANY tests!!!
'''
import logging
import jsonutil
import parsutil
import restutil
class JsonRestHelper(object):
prefix_to_ignore = '/'
__delete_parser = __put_parser = __post_parser = __get_parser = None
def hookup(self, handler):
""" "Hooks up" this helper instance to a handler object.
Args:
handler: an instance of a webapp.RequestHandler subclass
Side effects:
- sets self.handler to handler
- sets the handler's get, put, post and delete methods from self
- sets the handler's jrh attribute to self
Note this creates reference loops and MUST be undone in hookdown!
"""
logging.info('hookup %r/%r', self, handler)
self.handler = handler
handler.get = self.get
handler.put = self.put
handler.post = self.post
handler.delete = self.delete
handler.jrh = self
def hookdown(self):
""" Undoes the effects of self.hookup """
logging.info('hookdn %r/%r', self, self.handler)
h = self.handler
h.jrh = self.handler = None
del h.get, h.put, h.post, h.delete
def _serve(self, data):
""" Serves a result in JSON, and hooks-down from the handler """
try: return jsonutil.send_json(self.handler.response, data)
finally: self.hookdown()
def get_model(self, modelname):
""" Gets a model (or None) given a model name.
Args:
modelname: a string that should name a model
Returns:
a model class, or None (if no model's registered with that name)
Side effects:
sets response status to 400 if no model's registered with that name
"""
model = restutil.modelClassFromName(modelname)
if model is None:
self.handler.response.set_status(400, 'Model %r not found' % modelname)
return model
def get_special(self, specialname):
""" Gets a special (or None) given a special object's name.
Args:
specialname: a string that should name a special object
Returns:
a special object, or None (if no special's registered with that name)
Side effects:
sets response status to 400 if no special's registered with that name
"""
special = restutil.specialFromName(specialname)
if special is None:
self.handler.response.set_status(400, 'Special object %r not found' %
specialname)
return special
def get_entity(self, modelname, strid):
""" Gets an entity (or None) given a model name and entity ID as string.
Args:
modelname: a string that should name a model
strid: the str(id) for the numeric id of an entity of that model
Returns:
an entity, or None (if something went wrong)
Side effects:
sets response status to 400 or 404 if various things went wrong
"""
model = self.get_model(modelname)
if model is None:
return None
entity = model.get_by_id(int(strid))
if entity is None:
self.handler.response.set_status(404, "Entity %s/%s not found" %
(modelname, strid))
return entity
def get_special_method(self, specialname, methodname):
""" Gets a special object method (or None) given special & method names.
Args:
specialname: a string that should name a special object
methodname: a string that should name a method of that special object
Returns:
the method with that name in the special object of that name
Side effects:
sets response status to 400 if special or method not found
"""
special = self.get_special(specialname)
if special is None: return ''
method = special.get(methodname)
if method is None:
self.handler.response.set_status(400, 'Method %r not found in special %r'
% (methodname, specialname))
return method
def _methodhelper(self, modelname, methodname, _getter):
""" Gets a model or instance method given model and method names & getter.
Args:
modelname: a string that should name a model
methodname: a string that should name a method of that model
(model-method or instance-method, dep. on _getter)
Returns:
a method object, or None if either model or method were not found
Side effects:
sets response status to 400 if either model or method were not found
"""
model = self.get_model(modelname)
if model is None: return ''
method = _getter(model, methodname)
if method is None:
self.handler.response.set_status(400, 'Method %r not found in model' %
(methodname, modelname))
return method
def get_model_method(self, modelname, methodname):
""" Gets a model's method given model and method names.
Args:
modelname: a string that should name a model
methodname: a sring that should name a method of that model
Returns:
a method object, or None if either model or method were not found
Side effects:
sets response status to 400 if either model or method were not found
"""
return self._methodhelper(modelname, methodname, restutil.modelMethodByName)
def get_instance_method(self, modelname, methodname):
""" Gets an instance method given model and method names.
Args:
modelname: a string that should name a model
methodname: a sring that should name an instance method of that model
Returns:
a method object, or None if either model or method were not found
Side effects:
sets response status to 400 if either model or method were not found
"""
return self._methodhelper(modelname, methodname, restutil.instanceMethodByName)
def do_delete(self, model, strid):
""" Hook method to delete an entity given modelname and strid.
"""
entity = self.get_entity(model, strid)
if entity is not None:
entity.delete()
return {}
def delete(self, prefix=None):
""" Delete an entity given by path modelname/strid
Response is JSON for an empty jobj.
"""
if self.__delete_parser is None:
self.__delete_parser = parsutil.RestUrlParser(self.prefix_to_ignore,
do_model_strid=self.do_delete)
path = self.handler.request.path
result = self.__delete_parser.process(path, prefix)
if result is None or isinstance(result, tuple):
self.handler.response.set_status(400, 'Invalid URL for DELETE: %r' % path)
return self._serve(result)
def do_put(self, model, strid):
""" Hook method to update an entity given modelname and strid.
"""
entity = self.get_entity(model, strid)
if entity is None:
return {}
jobj = jsonutil.receive_json(self.handler.request)
jobj = jsonutil.update_entity(entity, jobj)
updated_entity_path = "/%s/%s" % (model, jobj['id'])
self.handler.response.set_status(200, 'Updated entity %s' %
updated_entity_path)
return jobj
def put(self, prefix=None):
""" Update an entity given by path modelname/strid
Request body is JSON for the needed changes
Response is JSON for the updated entity.
"""
if self.__put_parser is None:
self.__put_parser = parsutil.RestUrlParser(self.prefix_to_ignore,
do_model_strid=self.do_put)
path = self.handler.request.path
result = self.__put_parser.process(path, prefix)
if result is None or isinstance(result, tuple):
self.handler.response.set_status(400, 'Invalid URL for POST: %r' % path)
return self._serve({})
return self._serve(result)
def do_post_special_method(self, special, method):
""" Hook method to call a method on a special object given names.
"""
themethod = self.get_special_method(special, method)
if special is None: return ''
try: return themethod()
except Exception, e:
self.handler.response.set_status(400, "Can't call %r/%r: %s" % (
special, method, e))
return ''
def do_post_model(self, model):
""" Hook method to "call a model" (to create an entity)
"""
themodel = self.get_model(model)
if themodel is None: return ''
jobj = jsonutil.receive_json(self.handler.request)
jobj = jsonutil.make_entity(themodel, jobj)
self._classname = model
return jobj
def do_post_model_method(self, model, method):
""" Hook method to call a method on a model given s.
"""
themethod = self.get_model_method(model, method)
if themethod is None: return ''
try: return themethod()
except Exception, e:
self.handler.response.set_status(400, "Can't call %r/%r: %s" % (
model, method, e))
return ''
def do_post_entity_method(self, model, strid, method):
""" Hook method to call a method on an entity given s and strid.
"""
themethod = self.get_instance_method(model, method)
if themethod is None: return ''
entity = self.get_entity(model, strid)
if entity is None: return ''
try: return themethod(entity)
except Exception, e:
self.handler.response.set_status(400, "Can't call %r/%r/%r: %s" % (
model, strid, method, e))
return ''
def post(self, prefix=None):
""" Create an entity ("call a model") or perform other non-R/O "call".
Request body is JSON for the needed entity or other call "args".
Response is JSON for the updated entity (or "call result").
"""
if self.__post_parser is None:
self.__post_parser = parsutil.RestUrlParser(self.prefix_to_ignore,
do_special_method=self.do_post_special_method,
do_model=self.do_post_model,
do_model_method=self.do_post_model_method,
do_model_strid_method=self.do_post_entity_method,
)
path = self.handler.request.path
result = self.__post_parser.process(path, prefix)
if result is None or isinstance(result, tuple):
self.handler.response.set_status(400, 'Invalid URL for POST: %r' % path)
return self._serve({})
try:
strid = result['id']
except (KeyError, AttributeError, TypeError):
pass
else:
new_entity_path = "/%s/%s" % (self._classname, strid)
logging.info('Post (%r) created %r', path, new_entity_path)
self.handler.response.headers['Location'] = new_entity_path
self.handler.response.set_status(201, 'Created entity %s' %
new_entity_path)
return self._serve(result)
def do_get_special_method(self, special, method):
""" Hook method to R/O call a method on a special object given names.
"""
themethod = self.get_special_method(special, method)
if themethod is None: return ''
try: return themethod()
except Exception, e:
self.handler.response.set_status(400, "Can't call %r/%r: %s" % (
special, method, e))
return ''
def do_get_model(self, model):
""" Hook method to R/O "call a model" ("get list of all its IDs"...?)
"""
themodel = self.get_model(model)
if themodel is None: return ''
return [jsonutil.id_of(x) for x in themodel.all()]
def do_get_entity(self, model, strid):
""" Hook method to get data about an entity given model name and strid
"""
entity = self.get_entity(model, strid)
if entity is None:
return {}
return jsonutil.make_jobj(entity)
def do_get_model_method(self, model, method):
""" Hook method to R/O call a method on a model given s.
"""
themethod = self.get_model_method(model, method)
if themethod is None: return ''
try: return themethod()
except Exception, e:
self.handler.response.set_status(400, "Can't call %r/%r: %s" % (
model, method, e))
return ''
def do_get_entity_method(self, model, strid, method):
""" Hook method to R/O call a method on an entity given s and strid.
"""
themethod = self.get_instance_method(model, method)
if themethod is None: return ''
entity = self.get_entity(model, strid)
if entity is None: return ''
try: return themethod(entity)
except Exception, e:
self.handler.response.set_status(400, "Can't call %r/%r/%r: %s" % (
model, strid, method, e))
return ''
def get(self, prefix=None):
""" Get JSON data for entity IDs of a model, or all about an entity.
Depending on the request path, serve as JSON to the response object:
- for a path of /classname/id, a jobj for that entity
- for a path of /classname, a list of id-only jobjs for that model
- or, the results of the method being called (should be R/O!)
"""
logging.info('GET path=%r, prefix=%r', self.handler.request.path, prefix)
if self.__get_parser is None:
self.__get_parser = parsutil.RestUrlParser(self.prefix_to_ignore,
do_special_method=self.do_get_special_method,
do_model=self.do_get_model,
do_model_strid=self.do_get_entity,
do_model_method=self.do_get_model_method,
do_model_strid_method=self.do_get_entity_method,
)
path = self.handler.request.path
# hacky/kludgy special-case: serve all model names (TODO: remove this!)
# (need to have proper %meta special w/methods to get such info!)
if prefix is not None and path.strip('/') == prefix.strip('/'):
result = restutil.allModelClassNames()
logging.info('Hacky case (%r): %r', path, result)
return self._serve(result)
result = self.__get_parser.process(path, prefix)
if result is None or isinstance(result, tuple):
self.handler.response.set_status(400, 'Invalid URL for GET: %r' % path)
return self._serve({})
return self._serve(result)
# expose a single helper object, shd be reusable
helper = JsonRestHelper()
# just for testing...:
import wsgiref.handlers
from google.appengine.ext import webapp
import models
class _TestCrudRestHandler(webapp.RequestHandler):
def __init__(self, *a, **k):
webapp.RequestHandler.__init__(self, *a, **k)
helper.hookup(self)
def main():
logging.info('intgutil test main()')
application = webapp.WSGIApplication([('/(rest)/.*', _TestCrudRestHandler)],
debug=True)
wsgiref.handlers.CGIHandler().run(application)
if __name__ == '__main__':
main()