Merge lp:~pedronis/u1db/http-create-doc into lp:u1db

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
Reviewer Review Type Date Requested Status
John A Meinel (community) Approve
Review via email: mp+83406@code.launchpad.net

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.
Revision history for this message
John A Meinel (jameinel) wrote :

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)

Why not just do:

wire_description_to_exc = dict((x.wire_description, x) for x in globals().values()
                               if getattr(x, 'wire_description', None) is not None)

Seems to do the same thing, but simpler.

review: Approve
lp:~pedronis/u1db/http-create-doc updated
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)

Subscribers

People subscribed via source and target branches