Merge lp:~pedronis/u1db/u1db-client-http-ops into lp:u1db

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
Reviewer Review Type Date Requested Status
Ubuntu One hackers Pending
Review via email: mp+84147@code.launchpad.net

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 :

forgot, in the process added support for a couple more options to u1db-serve (host defaulting to localhost and working dir defaulting to .)

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+

Subscribers

People subscribed via source and target branches