Merge lp:~chromakode/boots/postgresql into lp:boots

Proposed by Max Goodhart
Status: Needs review
Proposed branch: lp:~chromakode/boots/postgresql
Merge into: lp:boots
Diff against target: 319 lines (+98/-32)
8 files modified
boots/api/constructors.py (+1/-1)
boots/api/errors.py (+4/-1)
boots/api/server.py (+59/-7)
boots/lib/console.py (+3/-3)
boots/lib/ui/plain.py (+16/-6)
tests/boots/api/server_tests.py (+12/-11)
tests/boots/boots_unit_test.py (+1/-1)
tests/boots/tests.py (+2/-2)
To merge this branch: bzr merge lp:~chromakode/boots/postgresql
Reviewer Review Type Date Requested Status
Boots Developers Pending
Review via email: mp+22358@code.launchpad.net

Description of the change

Initial PostgreSQL support added. Table output is wonky, because psycopg2 does not calculate display_size for columns, so we'd need to calculate the maximum sizes ourselves.

We can merge this code now, but it won't be very useful until DSN support lands, or the user is able to specify the server type some other way.

In a future patch, we should probably update gen_table() in plain.py to calculate the display size if the value we got is None.

To post a comment you must log in.
lp:~chromakode/boots/postgresql updated
163. By Max Goodhart

Add slow manual display_size calculation in output ascii table generation, in case display_size is not provided by the database driver.

Revision history for this message
Max Goodhart (chromakode) wrote :

And there's display_size calculation in gen_table(). Now PostgreSQL connections should be pretty indistinguishable from other connections.

lp:~chromakode/boots/postgresql updated
164. By Max Goodhart

Stylistic improvement to query code for PostgreSQL server_version.

Unmerged revisions

164. By Max Goodhart

Stylistic improvement to query code for PostgreSQL server_version.

163. By Max Goodhart

Add slow manual display_size calculation in output ascii table generation, in case display_size is not provided by the database driver.

162. By Max Goodhart

Initial internal PostgreSQL support, with the required tweaks and fixes.

161. By Max Goodhart

Refactor boots.api.api into boots.api.server.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'boots/api/constructors.py'
--- boots/api/constructors.py 2010-03-24 07:33:04 +0000
+++ boots/api/constructors.py 2010-03-29 08:42:30 +0000
@@ -23,7 +23,7 @@
2323
24import re24import re
2525
26from boots.api.api import Rows, ResultInfo26from boots.api.server import Rows, ResultInfo
27from boots.api.errors import BootsError27from boots.api.errors import BootsError
28from boots.api.nodes.node import NodeGraph, SyncNode, IteratorNode28from boots.api.nodes.node import NodeGraph, SyncNode, IteratorNode
29from boots.api import _, n_29from boots.api import _, n_
3030
=== modified file 'boots/api/errors.py'
--- boots/api/errors.py 2010-03-28 08:27:19 +0000
+++ boots/api/errors.py 2010-03-29 08:42:30 +0000
@@ -85,7 +85,10 @@
85 self.code = code85 self.code = code
86 86
87 def __str__(self):87 def __str__(self):
88 return "{msg} (#{code})".format(msg=self.msg, code=self.code)88 # Add the error code to the end of the first line of the error message.
89 lines = self.msg.split("\n", 1)
90 lines[0] ="{msg} (#{code})".format(msg=lines[0], code=self.code)
91 return "\n".join(lines)
89 92
90class ConnectionError(ServerError, CausedException):93class ConnectionError(ServerError, CausedException):
91 """Errors related to server connections."""94 """Errors related to server connections."""
9295
=== renamed file 'boots/api/api.py' => 'boots/api/server.py'
--- boots/api/api.py 2010-03-27 10:24:03 +0000
+++ boots/api/server.py 2010-03-29 08:42:30 +0000
@@ -39,11 +39,12 @@
39ColumnDescription = namedtuple("ColumnDescription", 39ColumnDescription = namedtuple("ColumnDescription",
40 ("name", "type_code", "display_size",40 ("name", "type_code", "display_size",
41 "internal_size", "precision", "scale",41 "internal_size", "precision", "scale",
42 "null_ok"))42 "null_ok", "index"))
4343
44class RowDescription:44class RowDescription:
45 def __init__(self, description):45 def __init__(self, description):
46 self.description = [ColumnDescription(*column) for column in description]46 self.description = [ColumnDescription(*column, index=index)
47 for index, column in enumerate(description)]
47 self.column_index = dict(zip((column[0] for column in description),48 self.column_index = dict(zip((column[0] for column in description),
48 range(len(description))))49 range(len(description))))
49 50
@@ -265,11 +266,6 @@
265 Server.__init__(self, MySQLdb, hostname, port, database, auth)266 Server.__init__(self, MySQLdb, hostname, port, database, auth)
266267
267 def _connect(self):268 def _connect(self):
268 """Establishes database connection.
269
270 Connect to the database server whose details were provided through
271 the __init__ method."""
272
273 converter = self.db.converters.conversions.copy()269 converter = self.db.converters.conversions.copy()
274 for x in range(0,256):270 for x in range(0,256):
275 converter.pop(x, None)271 converter.pop(x, None)
@@ -304,6 +300,62 @@
304else:300else:
305 _server_classes.append(MySQLdbServer)301 _server_classes.append(MySQLdbServer)
306302
303try:
304 import psycopg2
305 import psycopg2.extensions
306
307 # Disable psycopg2 type conversions
308 raw = psycopg2.extensions.new_type(tuple(psycopg2.extensions.string_types.keys()),
309 "raw",
310 lambda value, cur:value if value is not None else None)
311 psycopg2.extensions.register_type(raw)
312 psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
313
314 class PsycopgServer(Server):
315 def __init__(self, hostname, port, database, auth):
316 Server.__init__(self, psycopg2, hostname, port, database, auth)
317
318 def _connect(self):
319 connection = self.db.connect(
320 host = self.hostname,
321 port = self.port,
322 database = self.database,
323 user = self.auth.get("username") or "",
324 password = self.auth.get("password") or "")
325 connection.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
326 return connection
327
328 def _wrap_exception(self, e):
329 if isinstance(e, psycopg2.Error):
330 msg = e.pgerror
331 if msg is not None and msg.startswith("ERROR:"):
332 # Remove the ERROR: title.
333 msg = msg.replace("ERROR:", "", 1).strip()
334
335 if e.pgcode:
336 return errors.CodedDatabaseError(e.pgcode, msg, e)
337 else:
338 return errors.DatabaseError(msg, e)
339 else:
340 return e
341
342 @property
343 def server_version(self):
344 self._check_connected()
345 if hasattr(self._connection, "server_version"):
346 version = self._connection.server_version
347 return "{major}.{minor}.{revision}".format(major=version/10000,
348 minor=version/100%100,
349 revision=version%10000)
350 else:
351 # Versions of psycopg2 before 2.0.12 don't have the server_version property.
352 return self._execute("show server_version;").fetchone()[0]
353
354except ImportError:
355 PsycopgServer = None
356else:
357 _server_classes.append(PsycopgServer)
358
307def DrizzleServer(hostname, port, database, auth):359def DrizzleServer(hostname, port, database, auth):
308 if LibDrizzleServer is not None:360 if LibDrizzleServer is not None:
309 return LibDrizzleServer(hostname, port, database, auth)361 return LibDrizzleServer(hostname, port, database, auth)
310362
=== modified file 'boots/lib/console.py'
--- boots/lib/console.py 2010-03-28 23:55:27 +0000
+++ boots/lib/console.py 2010-03-29 08:42:30 +0000
@@ -28,7 +28,7 @@
28import os.path28import os.path
29import signal29import signal
3030
31from boots.api import api31from boots.api.server import DrizzleServer
32from boots.api.errors import BootsError, BootsWarning, CausedException, ConnectionError32from boots.api.errors import BootsError, BootsWarning, CausedException, ConnectionError
33from boots.api.nodes.node import NodeGraph, SyncNode, IteratorNode, LinkStackNode33from boots.api.nodes.node import NodeGraph, SyncNode, IteratorNode, LinkStackNode
34from boots.lib.ui.plain import PlainUI34from boots.lib.ui.plain import PlainUI
@@ -201,8 +201,8 @@
201 return result_data201 return result_data
202202
203 def connect(self, host, port, database=None, username=None, password=None):203 def connect(self, host, port, database=None, username=None, password=None):
204 server = api.DrizzleServer(host, port, database,204 server = DrizzleServer(host, port, database,
205 {"username": username, "password": password})205 {"username": username, "password": password})
206 server.connect()206 server.connect()
207 self.servers.append(server)207 self.servers.append(server)
208 return True208 return True
209209
=== modified file 'boots/lib/ui/plain.py'
--- boots/lib/ui/plain.py 2010-03-28 23:57:18 +0000
+++ boots/lib/ui/plain.py 2010-03-29 08:42:30 +0000
@@ -30,9 +30,9 @@
30import readline30import readline
31import subprocess31import subprocess
32import threading32import threading
33from itertools import izip33from itertools import imap
3434
35from boots.api.api import Rows, ResultInfo, DurationCounter35from boots.api.server import Rows, ResultInfo, DurationCounter
36from boots.api.errors import CausedException36from boots.api.errors import CausedException
37from boots.api.nodes.node import Status, NodeGraph, SyncNode, QueueNode, filter_none37from boots.api.nodes.node import Status, NodeGraph, SyncNode, QueueNode, filter_none
38from boots.lib.ui.components.help import HelpTopic, CommandHelpTopic38from boots.lib.ui.components.help import HelpTopic, CommandHelpTopic
@@ -184,13 +184,20 @@
184 conversion."""184 conversion."""
185 return value if value is not None else "NULL"185 return value if value is not None else "NULL"
186186
187 def display_size(column):
188 if column.display_size is not None:
189 return column.display_size
190 else:
191 # Welcome to slowville, population you.
192 return max(imap(len, (row[column.index] for row in self.buffer)))
193
187 def _gen_table(info):194 def _gen_table(info):
188 with DurationCounter() as table_elapsed:195 with DurationCounter() as table_elapsed:
189 if self.buffer:196 if self.buffer:
190 # If a column can contain the value NULL, we leave at least 4197 # If a column can contain the value NULL, we leave at least 4
191 # characters, since display_size does not account for NULL.198 # characters, since display_size does not account for NULL.
192 max_widths = map(max, [(len(column.name),199 max_widths = map(max, [(len(column.name),
193 column.display_size,200 display_size(column),
194 4 if column.null_ok else 0)201 4 if column.null_ok else 0)
195 for column in info["description"]])202 for column in info["description"]])
196 dashes = map(lambda x: "-"*(x+2), max_widths)203 dashes = map(lambda x: "-"*(x+2), max_widths)
@@ -210,9 +217,12 @@
210 "{count} rows in set",217 "{count} rows in set",
211 info["row_count"])218 info["row_count"])
212 else:219 else:
213 set_count = n_("{count} row affected",220 if info["row_count"] != -1:
214 "{count} rows affected",221 set_count = n_("{count} row affected",
215 info["row_count"])222 "{count} rows affected",
223 info["row_count"])
224 else:
225 set_count = _("Operation completed")
216226
217 nodes_elapsed = info["end_time"] - info["begin_time"] - info["server_elapsed"] 227 nodes_elapsed = info["end_time"] - info["begin_time"] - info["server_elapsed"]
218 timings = [_("{0:.2f}s server").format(info["server_elapsed"]),228 timings = [_("{0:.2f}s server").format(info["server_elapsed"]),
219229
=== renamed file 'tests/boots/api/api_tests.py' => 'tests/boots/api/server_tests.py'
--- tests/boots/api/api_tests.py 2010-03-07 10:59:51 +0000
+++ tests/boots/api/server_tests.py 2010-03-29 08:42:30 +0000
@@ -25,12 +25,13 @@
25import unittest25import unittest
26from itertools import ifilter26from itertools import ifilter
2727
28from boots.api import errors, api28from boots.api import errors
29from boots.api.server import DrizzleServer
29from boots.api.nodes.node import filter_default30from boots.api.nodes.node import filter_default
30import boots_unit_test31import boots_unit_test
3132
32class TestServerConnections(boots_unit_test.BootsQueryTester):33class TestServerConnections(boots_unit_test.BootsQueryTester):
33 """Class that provides unit tests that test the api.DrizzleServer class' ability34 """Class that provides unit tests that test the DrizzleServer class' ability
34 to connect to servers as well as provide proper errors when connections35 to connect to servers as well as provide proper errors when connections
35 should fail."""36 should fail."""
36 #FIXME should use the configuration provided.37 #FIXME should use the configuration provided.
@@ -40,54 +41,54 @@
40 pass41 pass
4142
42 def test_valid_connect_fqdn(self):43 def test_valid_connect_fqdn(self):
43 server = api.DrizzleServer("capstonedd.cs.pdx.edu", 9306, {})44 server = DrizzleServer("capstonedd.cs.pdx.edu", 9306, {})
44 server.connect()45 server.connect()
45 self.assert_(server.is_connected)46 self.assert_(server.is_connected)
46 server.disconnect()47 server.disconnect()
4748
48 def test_valid_connect_partial(self):49 def test_valid_connect_partial(self):
49 server = api.DrizzleServer("capstonedd", 9306, {})50 server = DrizzleServer("capstonedd", 9306, {})
50 server.connect()51 server.connect()
51 self.assert_(server.is_connected)52 self.assert_(server.is_connected)
52 server.disconnect()53 server.disconnect()
5354
54 def test_valid_connect_ip(self):55 def test_valid_connect_ip(self):
55 server = api.DrizzleServer("131.252.214.178", 9306, {})56 server = DrizzleServer("131.252.214.178", 9306, {})
56 server.connect()57 server.connect()
57 self.assert_(server.is_connected)58 self.assert_(server.is_connected)
58 server.disconnect()59 server.disconnect()
5960
60 def test_valid_connect_fqdn_with_db(self):61 def test_valid_connect_fqdn_with_db(self):
61 boots_unit_test.BootsQueryTester.setUp(self)62 boots_unit_test.BootsQueryTester.setUp(self)
62 server = api.DrizzleServer("capstonedd.cs.pdx.edu", 9306, {"database": "fec"})63 server = DrizzleServer("capstonedd.cs.pdx.edu", 9306, {"database": "fec"})
63 server.connect()64 server.connect()
64 self.assert_(server.is_connected)65 self.assert_(server.is_connected)
65 server.disconnect()66 server.disconnect()
6667
67 def test_invalid_connect(self):68 def test_invalid_connect(self):
68 server = api.DrizzleServer("kororaa.cs.pdx.edu", 9306, {})69 server = DrizzleServer("kororaa.cs.pdx.edu", 9306, {})
69 self.assertRaises(errors.ConnectionError, server.connect)70 self.assertRaises(errors.ConnectionError, server.connect)
70 self.assert_(not server.is_connected)71 self.assert_(not server.is_connected)
7172
72 def test_valid_disconnect(self):73 def test_valid_disconnect(self):
73 server = api.DrizzleServer("capstonedd.cs.pdx.edu", 9306, {})74 server = DrizzleServer("capstonedd.cs.pdx.edu", 9306, {})
74 server.connect()75 server.connect()
75 server.disconnect()76 server.disconnect()
76 self.assert_(not server.is_connected)77 self.assert_(not server.is_connected)
7778
78 def test_invalid_disconnect(self):79 def test_invalid_disconnect(self):
79 server = api.DrizzleServer("kororaa.cs.pdx.edu", 9306, {})80 server = DrizzleServer("kororaa.cs.pdx.edu", 9306, {})
80 self.assertRaises(errors.ConnectionError, server.connect)81 self.assertRaises(errors.ConnectionError, server.connect)
81 self.assertRaises(errors.ConnectionError, server.disconnect)82 self.assertRaises(errors.ConnectionError, server.disconnect)
82 self.assert_(not server.is_connected)83 self.assert_(not server.is_connected)
8384
84class TestServerExecute(boots_unit_test.BootsQueryTester):85class TestServerExecute(boots_unit_test.BootsQueryTester):
85 """Class that provides unit tests that tests api.DrizzleServer class' ability86 """Class that provides unit tests that tests DrizzleServer class' ability
86 to execute SQL statements on a server."""87 to execute SQL statements on a server."""
87 #FIXME needs to test more than just SELECT statements.88 #FIXME needs to test more than just SELECT statements.
88 def setUp(self):89 def setUp(self):
89 boots_unit_test.BootsQueryTester.setUp(self)90 boots_unit_test.BootsQueryTester.setUp(self)
90 self.server = api.DrizzleServer(self.config["host"], self.config["port"], {"database": self.config["database"]})91 self.server = DrizzleServer(self.config["host"], self.config["port"], {"database": self.config["database"]})
91 self.server.connect()92 self.server.connect()
9293
93 def tearDown(self):94 def tearDown(self):
9495
=== modified file 'tests/boots/boots_unit_test.py'
--- tests/boots/boots_unit_test.py 2010-03-07 10:59:51 +0000
+++ tests/boots/boots_unit_test.py 2010-03-29 08:42:30 +0000
@@ -59,7 +59,7 @@
59 fec_sql_path = os.path.join(base_tests_path, "fec.sql")59 fec_sql_path = os.path.join(base_tests_path, "fec.sql")
60 boots_path = os.path.join(base_tests_path, "../../bin/boots")60 boots_path = os.path.join(base_tests_path, "../../bin/boots")
61 61
62 cmd = "{boots} -H {host} -p {port} -f {file}".format(boots=boots_path,62 cmd = "{boots} -h {host} -p {port} -f {file}".format(boots=boots_path,
63 file=fec_sql_path, 63 file=fec_sql_path,
64 **self.config)64 **self.config)
65 p = Popen(cmd, shell=True)65 p = Popen(cmd, shell=True)
6666
=== modified file 'tests/boots/tests.py'
--- tests/boots/tests.py 2010-03-07 10:59:51 +0000
+++ tests/boots/tests.py 2010-03-29 08:42:30 +0000
@@ -35,7 +35,7 @@
35# Path mangling to import boots files properly.35# Path mangling to import boots files properly.
36new_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../..")36new_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../..")
37sys.path.insert(0, new_path)37sys.path.insert(0, new_path)
38from api import api_tests38from api import server_tests
39from api.nodes import node_tests39from api.nodes import node_tests
40from app import app_tests40from app import app_tests
41from lib import console_tests41from lib import console_tests
@@ -59,7 +59,7 @@
59 (options, args) = parser.parse_args()59 (options, args) = parser.parse_args()
60 BootsBaseTester.config.update(vars(options))60 BootsBaseTester.config.update(vars(options))
61 if not args:61 if not args:
62 modules = [api_tests, node_tests, app_tests,62 modules = [server_tests, node_tests, app_tests,
63 console_tests, lingo_tests, ui_tests]63 console_tests, lingo_tests, ui_tests]
64 else:64 else:
65 modules = map(globals().__getitem__, args)65 modules = map(globals().__getitem__, args)

Subscribers

People subscribed via source and target branches

to status/vote changes: