Merge lp:~pedronis/u1db/http-create-doc into lp:u1db
- http-create-doc
- Merge into trunk
Proposed by
Samuele Pedroni
Status: | Merged |
---|---|
Approved by: | Samuele Pedroni |
Approved revision: | 133 |
Merge reported by: | Samuele Pedroni |
Merged at revision: | not available |
Proposed branch: | lp:~pedronis/u1db/http-create-doc |
Merge into: | lp:u1db |
Diff against target: |
440 lines (+200/-29) 11 files modified
u1db/backends/inmemory.py (+2/-2) u1db/backends/sqlite_backend.py (+3/-3) u1db/errors.py (+28/-2) u1db/remote/http_app.py (+5/-1) u1db/remote/http_client.py (+11/-3) u1db/remote/http_database.py (+9/-2) u1db/tests/test_backends.py (+15/-15) u1db/tests/test_errors.py (+45/-0) u1db/tests/test_http_app.py (+30/-0) u1db/tests/test_http_client.py (+35/-1) u1db/tests/test_http_database.py (+17/-0) |
To merge this branch: | bzr merge lp:~pedronis/u1db/http-create-doc |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
John A Meinel (community) | Approve | ||
Review via email: mp+83406@code.launchpad.net |
Commit message
Description of the change
support create_doc for HTTPDatabase, the case when doc_id is not specified is implemented by locally generating a uuid
add support for transmitting errors back, and raising the corresponding exceptions again
rename InvalidDocRev to RevisionConflict as discussed with Stuart, it is transmitted back as status 409
add a generic HTTPError to deal with general protocol errors, server errors
To post a comment you must log in.
- 132. By Samuele Pedroni
-
xxx comment
- 133. By Samuele Pedroni
-
simplify, thanks jam
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'u1db/backends/inmemory.py' |
2 | --- u1db/backends/inmemory.py 2011-11-23 12:39:32 +0000 |
3 | +++ u1db/backends/inmemory.py 2011-11-25 15:07:25 +0000 |
4 | @@ -67,10 +67,10 @@ |
5 | raise errors.ConflictedDoc() |
6 | old_rev, old_content = self._docs[doc.doc_id] |
7 | if old_rev != doc.rev: |
8 | - raise errors.InvalidDocRev() |
9 | + raise errors.RevisionConflict() |
10 | else: |
11 | if doc.rev is not None: |
12 | - raise errors.InvalidDocRev() |
13 | + raise errors.RevisionConflict() |
14 | new_rev = self._allocate_doc_rev(doc.rev) |
15 | self._put_and_update_indexes(doc.doc_id, old_content, new_rev, |
16 | doc.content) |
17 | |
18 | === modified file 'u1db/backends/sqlite_backend.py' |
19 | --- u1db/backends/sqlite_backend.py 2011-11-23 13:46:56 +0000 |
20 | +++ u1db/backends/sqlite_backend.py 2011-11-25 15:07:25 +0000 |
21 | @@ -257,10 +257,10 @@ |
22 | if old_doc is not None: |
23 | old_content = old_doc.content |
24 | if old_doc.rev != doc.rev: |
25 | - raise errors.InvalidDocRev() |
26 | + raise errors.RevisionConflict() |
27 | else: |
28 | if doc.rev is not None: |
29 | - raise errors.InvalidDocRev() |
30 | + raise errors.RevisionConflict() |
31 | new_rev = self._allocate_doc_rev(doc.rev) |
32 | self._put_and_update_indexes(doc.doc_id, old_content, new_rev, |
33 | doc.content) |
34 | @@ -323,7 +323,7 @@ |
35 | if old_doc is None: |
36 | raise KeyError |
37 | if old_doc.rev != doc_rev: |
38 | - raise errors.InvalidDocRev() |
39 | + raise errors.RevisionConflict() |
40 | if old_doc.content is None: |
41 | raise KeyError |
42 | if self._has_conflicts(doc_id): |
43 | |
44 | === modified file 'u1db/errors.py' |
45 | --- u1db/errors.py 2011-11-23 10:12:18 +0000 |
46 | +++ u1db/errors.py 2011-11-25 15:07:25 +0000 |
47 | @@ -18,10 +18,18 @@ |
48 | class U1DBError(Exception): |
49 | """Generic base class for U1DB errors.""" |
50 | |
51 | - |
52 | -class InvalidDocRev(U1DBError): |
53 | + # description/tag for identifying the error during transmission (http,...) |
54 | + wire_description = "error" |
55 | + |
56 | + def __init__(self, message=None): |
57 | + self.message = message |
58 | + |
59 | + |
60 | +class RevisionConflict(U1DBError): |
61 | """The document revisions supplied does not match the current version.""" |
62 | |
63 | + wire_description = "revision conflict" |
64 | + |
65 | |
66 | class InvalidDocId(U1DBError): |
67 | """A document was tried with an invalid document identifier.""" |
68 | @@ -41,3 +49,21 @@ |
69 | |
70 | class DatabaseDoesNotExist(U1DBError): |
71 | """The database does not exist.""" |
72 | + |
73 | + |
74 | +class HTTPError(U1DBError): |
75 | + """Unspecific HTTP errror.""" |
76 | + |
77 | + wire_description = None |
78 | + |
79 | + def __init__(self, status, message=None): |
80 | + self.status = status |
81 | + self.message = message |
82 | + |
83 | + |
84 | +# mapping wire (transimission) descriptions/tags for errors to the exceptions |
85 | +wire_description_to_exc = dict((x.wire_description, x) |
86 | + for x in globals().values() |
87 | + if isinstance(x, type) and |
88 | + issubclass(x, U1DBError) and |
89 | + x.wire_description) |
90 | |
91 | === modified file 'u1db/remote/http_app.py' |
92 | --- u1db/remote/http_app.py 2011-11-24 13:38:48 +0000 |
93 | +++ u1db/remote/http_app.py 2011-11-25 15:07:25 +0000 |
94 | @@ -26,6 +26,7 @@ |
95 | from u1db import ( |
96 | __version__ as _u1db_version, |
97 | Document, |
98 | + errors, |
99 | ) |
100 | |
101 | |
102 | @@ -170,6 +171,7 @@ |
103 | def get(self): |
104 | self.responder.send_response(version=_u1db_version) |
105 | |
106 | + |
107 | @url_to_resource.register |
108 | class DocResource(object): |
109 | """Document resource.""" |
110 | @@ -377,10 +379,12 @@ |
111 | try: |
112 | resource = self._lookup_resource(environ, responder) |
113 | HTTPInvocationByMethodWithBody(resource, environ)() |
114 | + except (errors.RevisionConflict,), e: |
115 | + responder.send_response(409, error=e.wire_description) |
116 | except BadRequest: |
117 | # xxx introduce logging |
118 | #print environ['PATH_INFO'] |
119 | #import traceback |
120 | #traceback.print_exc() |
121 | - responder.send_response(400) |
122 | + responder.send_response(400, error="bad request") |
123 | return responder.content |
124 | |
125 | === modified file 'u1db/remote/http_client.py' |
126 | --- u1db/remote/http_client.py 2011-11-21 14:24:59 +0000 |
127 | +++ u1db/remote/http_client.py 2011-11-25 15:07:25 +0000 |
128 | @@ -19,6 +19,10 @@ |
129 | import urlparse |
130 | import urllib |
131 | |
132 | +from u1db import ( |
133 | + errors, |
134 | + ) |
135 | + |
136 | |
137 | class HTTPClientBase(object): |
138 | """Base class to make requests to a remote HTTP server.""" |
139 | @@ -44,9 +48,13 @@ |
140 | resp = self._conn.getresponse() |
141 | if resp.status in (200, 201): |
142 | return resp.read(), dict(resp.getheaders()) |
143 | - else: |
144 | - # xxx raise the proper exceptions depending on status |
145 | - raise Exception(resp.status) |
146 | + elif resp.status in (409,): |
147 | + respdic = simplejson.loads(resp.read()) |
148 | + exc_cls = errors.wire_description_to_exc.get(respdic.get("error")) |
149 | + if exc_cls is not None: |
150 | + message = respdic.get("message") |
151 | + raise exc_cls(message) |
152 | + raise errors.HTTPError(resp.status, resp.read()) |
153 | |
154 | def _request(self, method, url_parts, params=None, body=None, |
155 | content_type=None): |
156 | |
157 | === modified file 'u1db/remote/http_database.py' |
158 | --- u1db/remote/http_database.py 2011-11-18 14:10:09 +0000 |
159 | +++ u1db/remote/http_database.py 2011-11-25 15:07:25 +0000 |
160 | @@ -15,7 +15,7 @@ |
161 | """HTTPDabase to access a remote db over the HTTP API.""" |
162 | |
163 | import simplejson |
164 | - |
165 | +import uuid |
166 | |
167 | from u1db import ( |
168 | Database, |
169 | @@ -38,7 +38,6 @@ |
170 | doc.rev = res['rev'] |
171 | return res['rev'] |
172 | |
173 | - |
174 | def get_doc(self, doc_id): |
175 | res, headers = self._request('GET', ['doc', doc_id]) |
176 | doc_rev = headers['x-u1db-rev'] |
177 | @@ -46,3 +45,11 @@ |
178 | doc = Document(doc_id, doc_rev, res) |
179 | doc.has_conflicts = has_conflicts |
180 | return doc |
181 | + |
182 | + def create_doc(self, content, doc_id=None): |
183 | + if doc_id is None: |
184 | + doc_id = str(uuid.uuid4()) |
185 | + res, headers = self._request_json('PUT', ['doc', doc_id], {}, |
186 | + content, 'application/json') |
187 | + new_doc = Document(doc_id, res['rev'], content) |
188 | + return new_doc |
189 | |
190 | === modified file 'u1db/tests/test_backends.py' |
191 | --- u1db/tests/test_backends.py 2011-11-23 13:46:56 +0000 |
192 | +++ u1db/tests/test_backends.py 2011-11-25 15:07:25 +0000 |
193 | @@ -47,17 +47,6 @@ |
194 | 'server_def': http_server_def}), |
195 | ] |
196 | |
197 | - def test_put_doc_creating_initial(self): |
198 | - doc = Document('my_doc_id', None, simple_doc) |
199 | - new_rev = self.db.put_doc(doc) |
200 | - self.assertGetDoc(self.db, 'my_doc_id', new_rev, simple_doc, False) |
201 | - |
202 | - |
203 | -class LocalDatabaseTests(tests.DatabaseBaseTests): |
204 | - |
205 | - def test_close(self): |
206 | - self.db.close() |
207 | - |
208 | def test_create_doc_allocating_doc_id(self): |
209 | doc = self.db.create_doc(simple_doc) |
210 | self.assertNotEqual(None, doc.doc_id) |
211 | @@ -73,17 +62,28 @@ |
212 | def test_create_doc_existing_id(self): |
213 | doc = self.db.create_doc(simple_doc) |
214 | new_content = '{"something": "else"}' |
215 | - self.assertRaises(errors.InvalidDocRev, self.db.create_doc, |
216 | + self.assertRaises(errors.RevisionConflict, self.db.create_doc, |
217 | new_content, doc.doc_id) |
218 | self.assertGetDoc(self.db, doc.doc_id, doc.rev, simple_doc, False) |
219 | |
220 | + def test_put_doc_creating_initial(self): |
221 | + doc = Document('my_doc_id', None, simple_doc) |
222 | + new_rev = self.db.put_doc(doc) |
223 | + self.assertGetDoc(self.db, 'my_doc_id', new_rev, simple_doc, False) |
224 | + |
225 | + |
226 | +class LocalDatabaseTests(tests.DatabaseBaseTests): |
227 | + |
228 | + def test_close(self): |
229 | + self.db.close() |
230 | + |
231 | def test_put_doc_refuses_no_id(self): |
232 | doc = Document(None, None, simple_doc) |
233 | self.assertRaises(errors.InvalidDocId, self.db.put_doc, doc) |
234 | |
235 | def test_put_doc_refuses_non_existing_old_rev(self): |
236 | doc = Document('doc-id', 'test:4', simple_doc) |
237 | - self.assertRaises(errors.InvalidDocRev, self.db.put_doc, doc) |
238 | + self.assertRaises(errors.RevisionConflict, self.db.put_doc, doc) |
239 | |
240 | def test_get_docs(self): |
241 | doc1 = self.db.create_doc(simple_doc) |
242 | @@ -176,7 +176,7 @@ |
243 | old_rev = doc.rev |
244 | doc.rev = 'other:1' |
245 | doc.content = '{"something": "else"}' |
246 | - self.assertRaises(errors.InvalidDocRev, self.db.put_doc, doc) |
247 | + self.assertRaises(errors.RevisionConflict, self.db.put_doc, doc) |
248 | self.assertGetDoc(self.db, 'my_doc_id', old_rev, simple_doc, False) |
249 | |
250 | def test_delete_doc(self): |
251 | @@ -199,7 +199,7 @@ |
252 | def test_delete_doc_bad_rev(self): |
253 | doc = self.db.create_doc(simple_doc) |
254 | self.assertGetDoc(self.db, doc.doc_id, doc.rev, simple_doc, False) |
255 | - self.assertRaises(errors.InvalidDocRev, |
256 | + self.assertRaises(errors.RevisionConflict, |
257 | self.db.delete_doc, doc.doc_id, 'other:1') |
258 | self.assertGetDoc(self.db, doc.doc_id, doc.rev, simple_doc, False) |
259 | |
260 | |
261 | === added file 'u1db/tests/test_errors.py' |
262 | --- u1db/tests/test_errors.py 1970-01-01 00:00:00 +0000 |
263 | +++ u1db/tests/test_errors.py 2011-11-25 15:07:25 +0000 |
264 | @@ -0,0 +1,45 @@ |
265 | +# Copyright 2011 Canonical Ltd. |
266 | +# |
267 | +# This program is free software: you can redistribute it and/or modify it |
268 | +# under the terms of the GNU General Public License version 3, as published |
269 | +# by the Free Software Foundation. |
270 | +# |
271 | +# This program is distributed in the hope that it will be useful, but |
272 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
273 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
274 | +# PURPOSE. See the GNU General Public License for more details. |
275 | +# |
276 | +# You should have received a copy of the GNU General Public License along |
277 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
278 | + |
279 | +"""Tests error infrastructure.""" |
280 | + |
281 | +from u1db import ( |
282 | + errors, |
283 | + tests, |
284 | + ) |
285 | + |
286 | + |
287 | +class TestError(tests.TestCase): |
288 | + |
289 | + def test_error_base(self): |
290 | + err = errors.U1DBError() |
291 | + self.assertEqual("error", err.wire_description) |
292 | + self.assertIs(None, err.message) |
293 | + |
294 | + err = errors.U1DBError("Message.") |
295 | + self.assertEqual("error", err.wire_description) |
296 | + self.assertEqual("Message.", err.message) |
297 | + |
298 | + |
299 | + def test_HTTPError(self): |
300 | + err = errors.HTTPError(500) |
301 | + self.assertEqual(500, err.status) |
302 | + self.assertIs(None, err.wire_description) |
303 | + self.assertIs(None, err.message) |
304 | + |
305 | + err = errors.HTTPError(500, "Crash.") |
306 | + self.assertEqual(500, err.status) |
307 | + self.assertIs(None, err.wire_description) |
308 | + self.assertEqual("Crash.", err.message) |
309 | + |
310 | |
311 | === modified file 'u1db/tests/test_http_app.py' |
312 | --- u1db/tests/test_http_app.py 2011-11-21 14:24:59 +0000 |
313 | +++ u1db/tests/test_http_app.py 2011-11-25 15:07:25 +0000 |
314 | @@ -20,6 +20,7 @@ |
315 | |
316 | from u1db import ( |
317 | __version__ as _u1db_version, |
318 | + errors, |
319 | tests, |
320 | ) |
321 | |
322 | @@ -494,3 +495,32 @@ |
323 | self.assertEqual({'doc': '{"value": "there"}', |
324 | 'rev': doc.rev, 'id': doc.doc_id}, |
325 | simplejson.loads(parts[1])) |
326 | + |
327 | + |
328 | +class TestHTTPAppErrorHandling(tests.TestCase): |
329 | + |
330 | + def setUp(self): |
331 | + super(TestHTTPAppErrorHandling, self).setUp() |
332 | + self.exc = None |
333 | + self.state = tests.ServerStateForTests() |
334 | + class ErroringResource(object): |
335 | + |
336 | + def post(_, args, content): |
337 | + raise self.exc |
338 | + |
339 | + def lookup_resource(environ, responder): |
340 | + return ErroringResource() |
341 | + |
342 | + application = http_app.HTTPApp(self.state) |
343 | + application._lookup_resource = lookup_resource |
344 | + self.app = paste.fixture.TestApp(application) |
345 | + |
346 | + def test_RevisionConflict(self): |
347 | + self.exc = errors.RevisionConflict() |
348 | + resp = self.app.post('/req', params='{}', |
349 | + headers={'content-type': 'application/json'}, |
350 | + expect_errors=True) |
351 | + self.assertEqual(409, resp.status) |
352 | + self.assertEqual('application/json', resp.header('content-type')) |
353 | + self.assertEqual({"error": "revision conflict"}, |
354 | + simplejson.loads(resp.body)) |
355 | |
356 | === modified file 'u1db/tests/test_http_client.py' |
357 | --- u1db/tests/test_http_client.py 2011-11-23 09:34:59 +0000 |
358 | +++ u1db/tests/test_http_client.py 2011-11-25 15:07:25 +0000 |
359 | @@ -18,10 +18,11 @@ |
360 | from wsgiref import simple_server |
361 | |
362 | from u1db import ( |
363 | + errors, |
364 | tests, |
365 | ) |
366 | from u1db.remote import ( |
367 | - http_client |
368 | + http_client, |
369 | ) |
370 | |
371 | |
372 | @@ -38,6 +39,16 @@ |
373 | content_length = int(environ['CONTENT_LENGTH']) |
374 | ret['body'] = environ['wsgi.input'].read(content_length) |
375 | return [simplejson.dumps(ret)] |
376 | + elif environ['PATH_INFO'].endswith('error'): |
377 | + content_length = int(environ['CONTENT_LENGTH']) |
378 | + error = simplejson.loads(environ['wsgi.input'].read(content_length)) |
379 | + start_response(error['status'], |
380 | + [('Content-Type', 'application/json')]) |
381 | + response = error['response'] |
382 | + if isinstance(response, basestring): |
383 | + return [response] |
384 | + else: |
385 | + return [simplejson.dumps(error['response'])] |
386 | |
387 | def server_def(self): |
388 | def make_server(host_port, handler, state): |
389 | @@ -115,3 +126,26 @@ |
390 | 'QUERY_STRING': 'b=2', |
391 | 'body': '{"a": "x"}', |
392 | 'REQUEST_METHOD': 'POST'}, res) |
393 | + |
394 | + def test_unspecified_http_error(self): |
395 | + cli = self.getClient() |
396 | + self.assertRaises(errors.HTTPError, |
397 | + cli._request_json, 'POST', ['error'], {}, |
398 | + {'status': "500 Internal Error", |
399 | + 'response': "Crash."}) |
400 | + try: |
401 | + cli._request_json('POST', ['error'], {}, |
402 | + {'status': "500 Internal Error", |
403 | + 'response': "Fail."}) |
404 | + except errors.HTTPError, e: |
405 | + pass |
406 | + |
407 | + self.assertEqual(500, e.status) |
408 | + self.assertEqual("Fail.", e.message) |
409 | + |
410 | + def test_revision_conflict(self): |
411 | + cli = self.getClient() |
412 | + self.assertRaises(errors.RevisionConflict, |
413 | + cli._request_json, 'POST', ['error'], {}, |
414 | + {'status': "409 Conflict", |
415 | + 'response': {"error": "revision conflict"}}) |
416 | |
417 | === modified file 'u1db/tests/test_http_database.py' |
418 | --- u1db/tests/test_http_database.py 2011-11-23 09:34:59 +0000 |
419 | +++ u1db/tests/test_http_database.py 2011-11-25 15:07:25 +0000 |
420 | @@ -78,3 +78,20 @@ |
421 | self.assertGetDoc(self.db, 'doc-id', 'doc-rev', '{"v": 2}', False) |
422 | self.assertEqual(('GET', ['doc', 'doc-id'], None, None, None), |
423 | self.got) |
424 | + |
425 | + def test_create_doc_with_id(self): |
426 | + self.response_val = {'rev': 'doc-rev'}, {} |
427 | + new_doc = self.db.create_doc('{"v": 1}', doc_id='doc-id') |
428 | + self.assertEqual('doc-rev', new_doc.rev) |
429 | + self.assertEqual('doc-id', new_doc.doc_id) |
430 | + self.assertEqual('{"v": 1}', new_doc.content) |
431 | + self.assertEqual(('PUT', ['doc', 'doc-id'], {}, |
432 | + '{"v": 1}', 'application/json'), self.got) |
433 | + |
434 | + def test_create_doc_without_id(self): |
435 | + self.response_val = {'rev': 'doc-rev-2'}, {} |
436 | + new_doc = self.db.create_doc('{"v": 3}') |
437 | + self.assertEqual('doc-rev-2', new_doc.rev) |
438 | + self.assertEqual('{"v": 3}', new_doc.content) |
439 | + self.assertEqual(('PUT', ['doc', new_doc.doc_id], {}, |
440 | + '{"v": 3}', 'application/json'), self.got) |
84 +# mapping wire (transimission) descriptions/tags for errors to the exceptions on_to_exc = dict((x. wire_descriptio n, x)
85 +wire_descripti
86 + for x in globals().values()
87 + if isinstance(x, type) and
88 + issubclass(x, U1DBError) and
89 + x.wire_description)
Why not just do:
wire_descriptio n_to_exc = dict((x. wire_descriptio n, x) for x in globals().values()
if getattr(x, 'wire_description', None) is not None)
Seems to do the same thing, but simpler.