Merge lp:~pedronis/u1db/u1db-client-http-ops into lp:u1db
- u1db-client-http-ops
- Merge into trunk
Proposed by
Samuele Pedroni
Status: | Merged |
---|---|
Approved by: | John A Meinel |
Approved revision: | 139 |
Merged at revision: | 139 |
Proposed branch: | lp:~pedronis/u1db/u1db-client-http-ops |
Merge into: | lp:u1db |
Diff against target: |
410 lines (+176/-47) 5 files modified
u1db-serve (+8/-10) u1db/commandline/client.py (+37/-16) u1db/commandline/serve.py (+36/-0) u1db/tests/commandline/test_client.py (+69/-18) u1db/tests/commandline/test_serve.py (+26/-3) |
To merge this branch: | bzr merge lp:~pedronis/u1db/u1db-client-http-ops |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ubuntu One hackers | Pending | ||
Review via email: mp+84147@code.launchpad.net |
Commit message
Description of the change
support init-db, create, get, put, delete over http in u1db-client,
factor out the logic to open a local or remote db in a common base class, given that we have decided for now that u1db.open shouldn't deal with remote dbs,
for testing convenience, completeness and clarity, factor out the server building bits of u1db-serve to their own functions/file
To post a comment you must log in.
Revision history for this message
Samuele Pedroni (pedronis) wrote : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'u1db-serve' |
2 | --- u1db-serve 2011-11-22 11:09:00 +0000 |
3 | +++ u1db-serve 2011-12-01 17:29:35 +0000 |
4 | @@ -20,9 +20,8 @@ |
5 | from u1db import ( |
6 | __version__ as _u1db_version, |
7 | ) |
8 | -from u1db.remote import ( |
9 | - http_app, |
10 | - server_state, |
11 | +from u1db.commandline import ( |
12 | + serve, |
13 | ) |
14 | |
15 | |
16 | @@ -32,17 +31,16 @@ |
17 | description='Run the U1DB server') |
18 | p.add_argument('--version', action='version', version=_u1db_version) |
19 | p.add_argument('--verbose', action='store_true', help='be chatty') |
20 | + p.add_argument('--host', '-H', default='127.0.0.1', metavar='HOST', |
21 | + help='Bind on this host when serving.') |
22 | p.add_argument('--port', '-p', default=0, metavar='PORT', type=int, |
23 | help='Bind to this port when serving.') |
24 | + p.add_argument('--working-dir', default='.', metavar='WORKING_DIR', |
25 | + help='Directory where the databases live.') |
26 | |
27 | args = p.parse_args(args) |
28 | - |
29 | - state = server_state.ServerState() |
30 | - state.set_workingdir('.') |
31 | - application = http_app.HTTPApp(state) |
32 | - server = httpserver.WSGIServer(application, ('', args.port), |
33 | - httpserver.WSGIHandler) |
34 | - sys.stdout.write('listening on port: %s\n' % (server.server_address[1],)) |
35 | + server = serve.make_server(args.host, args.port, args.working_dir) |
36 | + sys.stdout.write('listening on: %s:%s\n' % server.server_address) |
37 | sys.stdout.flush() |
38 | server.serve_forever() |
39 | |
40 | |
41 | === modified file 'u1db/commandline/client.py' |
42 | --- u1db/commandline/client.py 2011-12-01 14:15:20 +0000 |
43 | +++ u1db/commandline/client.py 2011-12-01 17:29:35 +0000 |
44 | @@ -26,6 +26,7 @@ |
45 | from u1db.backends import sqlite_backend |
46 | from u1db.commandline import command |
47 | from u1db.remote import ( |
48 | + http_database, |
49 | http_target, |
50 | ) |
51 | |
52 | @@ -33,14 +34,26 @@ |
53 | client_commands = command.CommandGroup() |
54 | |
55 | |
56 | -class CmdCreate(command.Command): |
57 | +class OneDbCmd(command.Command): |
58 | + """Base class for commands operating on one local or remote database.""" |
59 | + |
60 | + def _open(self, database, create): |
61 | + if database.startswith('http://'): |
62 | + return http_database.HTTPDatabase.open_database(database, create) |
63 | + else: |
64 | + return u1db_open(database, create) |
65 | + |
66 | + |
67 | +class CmdCreate(OneDbCmd): |
68 | """Create a new document from scratch""" |
69 | |
70 | name = 'create' |
71 | |
72 | @classmethod |
73 | def _populate_subparser(cls, parser): |
74 | - parser.add_argument('database', help='The database to update') |
75 | + parser.add_argument('database', |
76 | + help='The local or remote database to update', |
77 | + metavar='database-path-or-url') |
78 | parser.add_argument('infile', nargs='?', default=None, |
79 | help='The file to read content from.') |
80 | parser.add_argument('--id', dest='doc_id', default=None, |
81 | @@ -49,27 +62,29 @@ |
82 | def run(self, database, infile, doc_id): |
83 | if infile is None: |
84 | infile = self.stdin |
85 | - db = u1db_open(database, create=False) |
86 | + db = self._open(database, create=False) |
87 | doc = db.create_doc(infile.read(), doc_id=doc_id) |
88 | self.stderr.write('id: %s\nrev: %s\n' % (doc.doc_id, doc.rev)) |
89 | |
90 | client_commands.register(CmdCreate) |
91 | |
92 | |
93 | -class CmdDelete(command.Command): |
94 | +class CmdDelete(OneDbCmd): |
95 | """Delete a document from the database""" |
96 | |
97 | name = 'delete' |
98 | |
99 | @classmethod |
100 | def _populate_subparser(cls, parser): |
101 | - parser.add_argument('database', help='The database to update') |
102 | + parser.add_argument('database', |
103 | + help='The local or remote database to update', |
104 | + metavar='database-path-or-url') |
105 | parser.add_argument('doc_id', help='The document id to retrieve') |
106 | parser.add_argument('doc_rev', |
107 | help='The revision of the document (which is being superseded.)') |
108 | |
109 | def run(self, database, doc_id, doc_rev): |
110 | - db = u1db_open(database, create=False) |
111 | + db = self._open(database, create=False) |
112 | doc = Document(doc_id, doc_rev, None) |
113 | db.delete_doc(doc) |
114 | self.stderr.write('rev: %s\n' % (doc.rev,)) |
115 | @@ -77,14 +92,16 @@ |
116 | client_commands.register(CmdDelete) |
117 | |
118 | |
119 | -class CmdGet(command.Command): |
120 | +class CmdGet(OneDbCmd): |
121 | """Extract a document from the database""" |
122 | |
123 | name = 'get' |
124 | |
125 | @classmethod |
126 | def _populate_subparser(cls, parser): |
127 | - parser.add_argument('database', help='The database to query') |
128 | + parser.add_argument('database', |
129 | + help='The local or remote database to query', |
130 | + metavar='database-path-or-url') |
131 | parser.add_argument('doc_id', help='The document id to retrieve.') |
132 | parser.add_argument('outfile', nargs='?', default=None, |
133 | help='The file to write the document to', |
134 | @@ -93,7 +110,7 @@ |
135 | def run(self, database, doc_id, outfile): |
136 | if outfile is None: |
137 | outfile = self.stdout |
138 | - db = u1db_open(database, create=False) |
139 | + db = self._open(database, create=False) |
140 | doc = db.get_doc(doc_id) |
141 | outfile.write(doc.content) |
142 | self.stderr.write('rev: %s\n' % (doc.rev,)) |
143 | @@ -105,33 +122,37 @@ |
144 | client_commands.register(CmdGet) |
145 | |
146 | |
147 | -class CmdInitDB(command.Command): |
148 | +class CmdInitDB(OneDbCmd): |
149 | """Create a new database""" |
150 | |
151 | name = 'init-db' |
152 | |
153 | @classmethod |
154 | def _populate_subparser(cls, parser): |
155 | - parser.add_argument('database', help='The database to create') |
156 | + parser.add_argument('database', |
157 | + help='The local or remote database to create', |
158 | + metavar='database-path-or-url') |
159 | parser.add_argument('--replica-uid', default=None, |
160 | - help='The unique identifier for this database') |
161 | + help='The unique identifier for this database (not for remote)') |
162 | |
163 | def run(self, database, replica_uid): |
164 | - db = u1db_open(database, create=True) |
165 | + db = self._open(database, create=True) |
166 | if replica_uid is not None: |
167 | db._set_replica_uid(replica_uid) |
168 | |
169 | client_commands.register(CmdInitDB) |
170 | |
171 | |
172 | -class CmdPut(command.Command): |
173 | +class CmdPut(OneDbCmd): |
174 | """Add a document to the database""" |
175 | |
176 | name = 'put' |
177 | |
178 | @classmethod |
179 | def _populate_subparser(cls, parser): |
180 | - parser.add_argument('database', help='The database to update') |
181 | + parser.add_argument('database', |
182 | + help='The local or remote database to update', |
183 | + metavar='database-path-or-url'), |
184 | parser.add_argument('doc_id', help='The document id to retrieve') |
185 | parser.add_argument('doc_rev', |
186 | help='The revision of the document (which is being superseded.)') |
187 | @@ -142,7 +163,7 @@ |
188 | def run(self, database, doc_id, doc_rev, infile): |
189 | if infile is None: |
190 | infile = self.stdin |
191 | - db = u1db_open(database, create=False) |
192 | + db = self._open(database, create=False) |
193 | doc = Document(doc_id, doc_rev, infile.read()) |
194 | doc_rev = db.put_doc(doc) |
195 | self.stderr.write('rev: %s\n' % (doc_rev,)) |
196 | |
197 | === added file 'u1db/commandline/serve.py' |
198 | --- u1db/commandline/serve.py 1970-01-01 00:00:00 +0000 |
199 | +++ u1db/commandline/serve.py 2011-12-01 17:29:35 +0000 |
200 | @@ -0,0 +1,36 @@ |
201 | +# Copyright 2011 Canonical Ltd. |
202 | +# |
203 | +# This program is free software: you can redistribute it and/or modify it |
204 | +# under the terms of the GNU General Public License version 3, as published |
205 | +# by the Free Software Foundation. |
206 | +# |
207 | +# This program is distributed in the hope that it will be useful, but |
208 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
209 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
210 | +# PURPOSE. See the GNU General Public License for more details. |
211 | +# |
212 | +# You should have received a copy of the GNU General Public License along |
213 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
214 | + |
215 | +"""Build server for u1db-serve.""" |
216 | + |
217 | +from paste import httpserver |
218 | + |
219 | +from u1db import ( |
220 | + __version__ as _u1db_version, |
221 | + ) |
222 | +from u1db.remote import ( |
223 | + http_app, |
224 | + server_state, |
225 | + ) |
226 | + |
227 | + |
228 | +def make_server(host, port, working_dir): |
229 | + """Make a server on host and port exposing dbs living in working_dir.""" |
230 | + state = server_state.ServerState() |
231 | + state.set_workingdir(working_dir) |
232 | + application = http_app.HTTPApp(state) |
233 | + server = httpserver.WSGIServer(application, (host, port), |
234 | + httpserver.WSGIHandler) |
235 | + return server |
236 | + |
237 | |
238 | === modified file 'u1db/tests/commandline/test_client.py' |
239 | --- u1db/tests/commandline/test_client.py 2011-12-01 14:53:45 +0000 |
240 | +++ u1db/tests/commandline/test_client.py 2011-12-01 17:29:35 +0000 |
241 | @@ -23,7 +23,10 @@ |
242 | open as u1db_open, |
243 | tests, |
244 | ) |
245 | -from u1db.commandline import client |
246 | +from u1db.commandline import ( |
247 | + client, |
248 | + serve, |
249 | + ) |
250 | from u1db.tests.commandline import safe_close |
251 | from u1db.tests import test_remote_sync_target |
252 | |
253 | @@ -287,8 +290,25 @@ |
254 | self.assertGetDoc(self.db, doc2.doc_id, doc2.rev, tests.nested_doc, |
255 | False) |
256 | |
257 | - |
258 | -class TestCommandLine(TestCaseWithDB): |
259 | +class RunMainHelper(object): |
260 | + |
261 | + def run_main(self, args, stdin=None): |
262 | + if stdin is not None: |
263 | + self.patch(sys, 'stdin', cStringIO.StringIO(stdin)) |
264 | + stdout = cStringIO.StringIO() |
265 | + stderr = cStringIO.StringIO() |
266 | + self.patch(sys, 'stdout', stdout) |
267 | + self.patch(sys, 'stderr', stderr) |
268 | + try: |
269 | + ret = client.main(args) |
270 | + except SystemExit, e: |
271 | + self.fail("Intercepted SystemExit: %s" % (e,)) |
272 | + if ret is None: |
273 | + ret = 0 |
274 | + return ret, stdout.getvalue(), stderr.getvalue() |
275 | + |
276 | + |
277 | +class TestCommandLine(TestCaseWithDB, RunMainHelper): |
278 | """These are meant to test that the infrastructure is fully connected. |
279 | |
280 | Each command is likely to only have one test here. Something that ensures |
281 | @@ -309,21 +329,6 @@ |
282 | self.addCleanup(safe_close, p) |
283 | return p |
284 | |
285 | - def run_main(self, args, stdin=None): |
286 | - if stdin is not None: |
287 | - self.patch(sys, 'stdin', cStringIO.StringIO(stdin)) |
288 | - stdout = cStringIO.StringIO() |
289 | - stderr = cStringIO.StringIO() |
290 | - self.patch(sys, 'stdout', stdout) |
291 | - self.patch(sys, 'stderr', stderr) |
292 | - try: |
293 | - ret = client.main(args) |
294 | - except SystemExit, e: |
295 | - self.fail("Intercepted SystemExit: %s" % (e,)) |
296 | - if ret is None: |
297 | - ret = 0 |
298 | - return ret, stdout.getvalue(), stderr.getvalue() |
299 | - |
300 | def test_create_subprocess(self): |
301 | p = self.runU1DBClient(['create', '--id', 'test-id', self.db_path]) |
302 | stdout, stderr = p.communicate(tests.simple_doc) |
303 | @@ -379,3 +384,49 @@ |
304 | self.assertEqual('', stdout) |
305 | self.assertEqual('', stderr) |
306 | self.assertGetDoc(self.db2, 'test-id', doc.rev, tests.simple_doc, False) |
307 | + |
308 | + |
309 | +class TestHTTPIntegration(tests.TestCaseWithServer, RunMainHelper): |
310 | + """Meant to test the cases where commands operate over http.""" |
311 | + |
312 | + def server_def(self): |
313 | + def make_server(host_port, handler, _state): |
314 | + return serve.make_server(host_port[0], host_port[1], |
315 | + self.working_dir) |
316 | + return make_server, None, "shutdown", "http" |
317 | + |
318 | + def setUp(self): |
319 | + super(TestHTTPIntegration, self).setUp() |
320 | + self.working_dir = self.createTempDir(prefix='u1db-http-server-') |
321 | + self.startServer() |
322 | + |
323 | + def getPath(self, dbname): |
324 | + return os.path.join(self.working_dir, dbname) |
325 | + |
326 | + def test_init_db(self): |
327 | + url = self.getURL('new.db') |
328 | + ret, stdout, stderr = self.run_main(['init-db', url]) |
329 | + db2 = u1db_open(self.getPath('new.db'), create=False) |
330 | + |
331 | + def test_create_get_put_delete(self): |
332 | + db = u1db_open(self.getPath('test.db'), create=True) |
333 | + url = self.getURL('test.db') |
334 | + doc_id = '%abcd' |
335 | + ret, stdout, stderr = self.run_main(['create', url, '--id', doc_id], |
336 | + stdin=tests.simple_doc) |
337 | + self.assertEqual(0, ret) |
338 | + ret, stdout, stderr = self.run_main(['get', url, doc_id]) |
339 | + self.assertEqual(0, ret) |
340 | + self.assertTrue(stderr.startswith('rev: ')) |
341 | + doc_rev = stderr[len('rev: '):].rstrip() |
342 | + ret, stdout, stderr = self.run_main(['put', url, doc_id, doc_rev], |
343 | + stdin=tests.nested_doc) |
344 | + self.assertEqual(0, ret) |
345 | + self.assertTrue(stderr.startswith('rev: ')) |
346 | + doc_rev1 = stderr[len('rev: '):].rstrip() |
347 | + self.assertGetDoc(db, doc_id, doc_rev1, tests.nested_doc, False) |
348 | + ret, stdout, stderr = self.run_main(['delete', url, doc_id, doc_rev1]) |
349 | + self.assertEqual(0, ret) |
350 | + self.assertTrue(stderr.startswith('rev: ')) |
351 | + doc_rev2 = stderr[len('rev: '):].rstrip() |
352 | + self.assertGetDoc(db, doc_id, doc_rev2, None, False) |
353 | |
354 | === modified file 'u1db/tests/commandline/test_serve.py' |
355 | --- u1db/tests/commandline/test_serve.py 2011-11-23 09:34:59 +0000 |
356 | +++ u1db/tests/commandline/test_serve.py 2011-12-01 17:29:35 +0000 |
357 | @@ -19,6 +19,7 @@ |
358 | |
359 | from u1db import ( |
360 | __version__ as _u1db_version, |
361 | + open as u1db_open, |
362 | tests, |
363 | ) |
364 | from u1db.remote import http_client |
365 | @@ -49,10 +50,10 @@ |
366 | |
367 | def test_bind_to_port(self): |
368 | p = self.startU1DBServe([]) |
369 | - starts = 'listening on port:' |
370 | + starts = 'listening on:' |
371 | x = p.stdout.readline() |
372 | self.assertTrue(x.startswith(starts)) |
373 | - port = int(x[len(starts):]) |
374 | + port = int(x[len(starts):].split(":")[1]) |
375 | url = "http://127.0.0.1:%s/" % port |
376 | c = http_client.HTTPClientBase(url) |
377 | self.addCleanup(c.close) |
378 | @@ -66,9 +67,31 @@ |
379 | s.close() |
380 | p = self.startU1DBServe(['--port', str(port)]) |
381 | x = p.stdout.readline().strip() |
382 | - self.assertEqual('listening on port: %s' % (port,), x) |
383 | + self.assertEqual('listening on: 127.0.0.1:%s' % (port,), x) |
384 | url = "http://127.0.0.1:%s/" % port |
385 | c = http_client.HTTPClientBase(url) |
386 | self.addCleanup(c.close) |
387 | res, _ = c._request_json('GET', []) |
388 | self.assertEqual({'version': _u1db_version}, res) |
389 | + |
390 | + def test_bind_to_host(self): |
391 | + p = self.startU1DBServe(["--host", "localhost"]) |
392 | + starts = 'listening on: 127.0.0.1:' |
393 | + x = p.stdout.readline() |
394 | + self.assertTrue(x.startswith(starts)) |
395 | + |
396 | + def test_supply_working_dir(self): |
397 | + tmp_dir = self.createTempDir('u1db-serve-test') |
398 | + db = u1db_open(os.path.join(tmp_dir, 'landmark.db'), create=True) |
399 | + db.close() |
400 | + p = self.startU1DBServe(['--working-dir', tmp_dir]) |
401 | + starts = 'listening on:' |
402 | + x = p.stdout.readline() |
403 | + self.assertTrue(x.startswith(starts)) |
404 | + port = int(x[len(starts):].split(":")[1]) |
405 | + url = "http://127.0.0.1:%s/landmark.db" % port |
406 | + c = http_client.HTTPClientBase(url) |
407 | + self.addCleanup(c.close) |
408 | + res, _ = c._request_json('GET', []) |
409 | + self.assertEqual({}, res) |
410 | + |
forgot, in the process added support for a couple more options to u1db-serve (host defaulting to localhost and working dir defaulting to .)