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
21
22
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):
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
64
65 @werkzeug.wrappers.cached_property
67 """
68 @returns: a reasonable dump format
69 @rtype: string
70
71 @raises: various errors if things are confused
72 """
73
74 format=self.args.get('format')
75
76 if format is None:
77
78 accept=self.headers.get('Accept', '').lower()
79
80
81 try:
82 accept_mimetype=mimeparse.best_match(self.application.mimetype2format.keys(), accept)
83 except ValueError:
84
85 raise exceptions.BadRequest()
86
87
88 format=self.application.mimetype2format[accept_mimetype] if accept_mimetype else None
89
90
91 if format not in self.application.dumpSpatch: raise exceptions.NotAcceptable()
92
93 return format
94
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
122 """a view for RESTful web apps"""
123 @property
125 """a Map for the view"""
126
127 rules=[Rule('/__<reserved_name>__/', endpoint='reserved')]
128 return Map(self.rules()+rules)
129
131 """
132 @returns: rules for the view
133 @rypte: list of L{Rule}
134 """
135 raise NotImplementedError
136
137 @staticmethod
139 """called for unknown /__reserved__/ endpoint"""
140 raise exceptions.NotFound()
141
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
167
168 }
169
170 requestType=RestRequest
171
172 - def __init__(self, view, loadSpatch, dumpSpatch):
173
174
175
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
187 self.__fix_method(environ)
188
189
190 urls = self.view.url_map.bind_to_environ(environ)
191 endpoint, args = urls.match()
192
193
194 request=self.requestType(environ, endpoint=endpoint, application=self)
195
196
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
215 @staticmethod
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