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

Source Code for Module grassyknoll.frontend.EasyRest

  1  """Helper code to make writing RESTful web apps suck less. 
  2   
  3  But only a little less. 
  4   
  5  The organization of these classes is somewhat arbitrary. I blame the libraries 
  6  (wsgi & werkzeug). 
  7  """ 
  8   
  9  from __future__ import with_statement 
 10   
 11  from grassyknoll.compat import mimeparse 
 12  from grassyknoll.lib.meta import Spatch 
 13   
 14  from werkzeug.wrappers import BaseRequest, BaseResponse, CommonResponseDescriptorsMixin 
 15  import werkzeug.wrappers 
 16  from werkzeug import exceptions 
 17  from werkzeug.routing import Map, Rule 
 18   
 19   
 20  ## Whee, let's work around a whole bunch of premature optimization, dumb 
 21  ## internal apis and read-only B&D crap! Come on, we're all adults here. 
 22   
23 -class RestRequest(BaseRequest):
24 """a HTTP Request object for writing RESTful web apps. 25 26 @ivar application: The application this request is running under 27 @type application: RestApplication 28 29 @ivar endpoint: the name of the current UrlMap endpoint 30 @type endpoint: string 31 """ 32
33 - def __init__(self, environ, application, endpoint, **kwargs):
34 assert isinstance(application, RestApplication) 35 self.application=application 36 self.endpoint=endpoint 37 super(RestRequest, self).__init__(environ, **kwargs)
38 39 @werkzeug.wrappers.cached_property
40 - def content_type(self):
41 """the Content-Type, if method is POST/PUT""" 42 return self.environ['CONTENT_TYPE'].lower() if self.method in ('POST', 'PUT') else None
43 44 @werkzeug.wrappers.cached_property
45 - def load_format(self):
46 """ 47 @returns: a reasonable load format 48 @rtype: string 49 50 @raises: various errors if things are confused 51 """ 52 ## slurp out the content_type mimetype 53 content_type=self.content_type 54 if not content_type: return None 55 56 ## turn it into a format 57 format=self.application.mimetype2format.get(content_type) 58 59 ## a format we don't know how to load 60 if format not in self.application.loadSpatch: 61 raise exceptions.UnsupportedMediaType() 62 63 return format
64 65 @werkzeug.wrappers.cached_property
66 - def dump_format(self):
67 """ 68 @returns: a reasonable dump format 69 @rtype: string 70 71 @raises: various errors if things are confused 72 """ 73 ## if we have &format=, just use that 74 format=self.args.get('format') 75 76 if format is None: 77 ## get a reasonable value for accept 78 accept=self.headers.get('Accept', '').lower() 79 80 ## compare to mimetypes 81 try: 82 accept_mimetype=mimeparse.best_match(self.application.mimetype2format.keys(), accept) 83 except ValueError: 84 ## Accept header is just horked 85 raise exceptions.BadRequest() 86 87 ## if mimeparse matched, get the corresponding format 88 format=self.application.mimetype2format[accept_mimetype] if accept_mimetype else None 89 90 ## a format we don't know how to dump 91 if format not in self.application.dumpSpatch: raise exceptions.NotAcceptable() 92 93 return format
94
95 - def load(self):
96 """load plain python out of the request""" 97 loader=self.application.loadSpatch[self.load_format] 98 return loader(self)
99
100 - def dump(self, obj):
101 """ 102 @arg request: the current request 103 @type request: L{Request} 104 105 @arg obj: an object to dump 106 @type obj: object 107 108 @rtype: L{Response} 109 @raises: various errors 110 """ 111 dumper=self.application.dumpSpatch[self.dump_format] 112 return dumper(self, obj)
113
114 -class RestResponse(BaseResponse, CommonResponseDescriptorsMixin):
115 """a HTTP Responseobject for writing RESTful web apps. 116 117 wtf mixin crap 118 """ 119 pass
120
121 -class RestView(object):
122 """a view for RESTful web apps""" 123 @property
124 - def url_map(self):
125 """a Map for the view""" 126 ## 404 all __magic__ names 127 rules=[Rule('/__<reserved_name>__/', endpoint='reserved')] 128 return Map(self.rules()+rules)
129
130 - def rules(self):
131 """ 132 @returns: rules for the view 133 @rypte: list of L{Rule} 134 """ 135 raise NotImplementedError
136 137 @staticmethod
138 - def reserved(request, reserved_name):
139 """called for unknown /__reserved__/ endpoint""" 140 raise exceptions.NotFound()
141
142 -class RestApplication(object):
143 """A WSGI application for writing RESTful webapps 144 145 Instances of the class are WSGI callables (ie, take 146 environ/start_response). 147 148 @cvar mimetype2format: a mapping of mimetypes to serialization formats 149 @type mimetype2format: dict 150 151 @cvar requestType: class to use for creating Requests. 152 @type requestType: L{RestRequest} subclass 153 154 @ivar loadSpatch: dispatch for loading data from Requests 155 @type loadSpatch: L{Spatch} 156 157 @ivar dumpSpatch: dispatch for dumping data to Responses 158 @type dumpSpatch: L{Spatch} 159 160 @ivar view: a view implementing the application 161 @type view: RestView 162 """ 163 mimetype2format={'application/json':'json', 164 'text/html':'html', 165 'text/plain':'plain', 166 #'application/x-www-form-urlencoded':'urlencoded', 167 #'multipart/form-data':'formdata', 168 } 169 170 requestType=RestRequest 171
172 - def __init__(self, view, loadSpatch, dumpSpatch):
173 174 # XXX it'd be nice to specify the function to call in the Rule, but 175 # that would require sublcassing Rule. Horror. 176 self.view=view 177 178 assert isinstance(loadSpatch, Spatch) 179 assert isinstance(dumpSpatch, Spatch) 180 181 self.loadSpatch=loadSpatch 182 self.dumpSpatch=dumpSpatch
183
184 - def __call__(self, environ, start_response):
185 try: 186 ## fix up METHOD 187 self.__fix_method(environ) 188 189 ## parse urls 190 urls = self.view.url_map.bind_to_environ(environ) 191 endpoint, args = urls.match() 192 193 ## build a nice request object 194 request=self.requestType(environ, endpoint=endpoint, application=self) 195 196 ## map endpoint to view method call 197 view=getattr(self.view, endpoint, None) 198 if view is None: raise exceptions.NotImplemented() 199 200 response=view(request, **args) 201 assert isinstance(response, BaseResponse) 202 203 except exceptions.HTTPException, e: 204 return self.onError(e, environ, start_response) 205 else: 206 return response(environ, start_response) 207 finally: 208 pass
209
210 - def onError(self, error, environ, start_response):
211 """handle HTTP Exceptions""" 212 return error(environ, start_response)
213 214 ## internal utility methods ## 215 @staticmethod
216 - def __fix_method(environ):
217 """Contract: &method=FOO + POST ==> REQUEST_METHOD=FOO""" 218 request=BaseRequest(environ, populate_request=False) 219 arg_method=request.args.get('method', '').upper() 220 if not arg_method: return 221 222 if request.method != 'POST': 223 raise exceptions.MethodNotAllowed(valid_methods=['POST']) 224 else: 225 environ['REQUEST_METHOD']=arg_method
226