I was recently contacted about whether or not there were any wxPython applications out here that could present a SQLite database. As I understood it, they wanted to be able to introspect the database and view the tables, probably using the wx.grid.Grid widget. I find the Grid widget to be very powerful and also rather difficult to use. So I spent some time writing a super simple application that uses the ObjectListView widget instead.
Getting Started
First off, we need a database to test with. So I ended up coding up a simple database creation script with SQLAlchemy that populates a couple tables with a couple rows. Here’s the script I’m using:
from sqlalchemy import create_engine from sqlalchemy.orm import relationship, backref, sessionmaker from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import Column, ForeignKey, Integer, String engine = create_engine('sqlite:///example.db', echo=True) Base = declarative_base() ######################################################################## class Book(Base): """""" __tablename__ = "books" id = Column(Integer, primary_key = True) title = Column(String) author = Column(String) #---------------------------------------------------------------------- def __init__(self, title, author): """Constructor""" self.title = title self.author = author ######################################################################## class Character(Base): """""" __tablename__ = "characters" id = Column(Integer, primary_key = True) first_name = Column(String) last_name = Column(String) book_id = Column(ForeignKey("books.id")) book = relationship("Book", backref=backref("characters", order_by=id)) #---------------------------------------------------------------------- def __init__(self, first_name, last_name): """Constructor""" self.first_name = first_name self.last_name = last_name #---------------------------------------------------------------------- @property def fullname(self): """""" return "%s %s" % (self.first_name, self.last_name) #---------------------------------------------------------------------- def __repr__(self): """""" return "" % self.fullname Base.metadata.create_all(engine) Session = sessionmaker(bind=engine) session = Session() print new_char = Character("Hermione", "Granger") new_char.book = Book("Harry Potter", "JK Rowling") session.add(new_char) new_char = Character("Sherlock", "Holmes") new_char.book = Book("The Adventure of the Creeping Man", "Arthur Conan Doyle") session.add(new_char) session.commit()
I’m going to assume you understand SQLAlchemy well enough to follow this. If not, they have some of the best documentation of any Python project I’ve ever used.
Creating the Viewer
Now we just need to create the database table viewer. This took me a little tinkering, but I eventually figured it out. Please note that it’s basically alpha quality and doesn’t have any error checking in it.
import os import wx from ObjectListView import ObjectListView, ColumnDefn from sqlalchemy import create_engine, MetaData, Table from sqlalchemy.orm import mapper, sessionmaker, clear_mappers ######################################################################## class GenericDBClass(object): """""" pass ######################################################################## class MainPanel(wx.Panel): #---------------------------------------------------------------------- def __init__(self, parent): wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY) self.db_data = [] self.current_directory = os.getcwd() self.dataOlv = ObjectListView(self, wx.ID_ANY, style=wx.LC_REPORT|wx.SUNKEN_BORDER) self.dataOlv.Hide() # Allow the cell values to be edited when double-clicked self.dataOlv.cellEditMode = ObjectListView.CELLEDIT_SINGLECLICK # load DB loadDBBtn = wx.Button(self, label="Load DB") loadDBBtn.Bind(wx.EVT_BUTTON, self.loadDatabase) self.table_names = [] self.tableCbo = wx.ComboBox(self, value="", choices=self.table_names) self.tableCbo.Bind(wx.EVT_COMBOBOX, self.loadTable) # Create some sizers mainSizer = wx.BoxSizer(wx.VERTICAL) mainSizer.Add(loadDBBtn, 0, wx.ALL|wx.CENTER, 5) mainSizer.Add(self.tableCbo, 0, wx.ALL|wx.CENTER, 5) mainSizer.Add(self.dataOlv, 1, wx.ALL|wx.EXPAND, 5) self.SetSizer(mainSizer) #---------------------------------------------------------------------- def loadTable(self, event): """""" print current_table = self.tableCbo.GetValue() metadata = MetaData(self.engine) table = Table(current_table, metadata, autoload=True, autoload_with=self.engine) self.columns = table.columns.keys() clear_mappers() #http://docs.sqlalchemy.org/en/rel_0_6/orm/mapper_config.html#sqlalchemy.orm.clear_mappers mapper(GenericDBClass, table) Session = sessionmaker(bind=self.engine) session = Session() self.db_data = session.query(GenericDBClass).all() self.setData() self.dataOlv.Show() self.Layout() #---------------------------------------------------------------------- def loadDatabase(self, event): """""" wildcard = "All files (*.*)|*.*" dlg = wx.FileDialog( self, message="Choose a file", defaultDir=self.current_directory, defaultFile="", wildcard=wildcard, style=wx.OPEN | wx.CHANGE_DIR ) if dlg.ShowModal() == wx.ID_OK: db_path = dlg.GetPath() dlg.Destroy() else: dlg.Destroy() return self.engine = create_engine('sqlite:///%s' % db_path, echo=True) self.table_names = self.engine.table_names() self.tableCbo.SetItems(self.table_names) self.tableCbo.SetValue(self.table_names[0]) self.loadTable("") #---------------------------------------------------------------------- def setData(self, data=None): olv_columns = [] for column in self.columns: olv_columns.append(ColumnDefn(column.title(), "left", 120, column.lower())) self.dataOlv.SetColumns(olv_columns) self.dataOlv.SetObjects(self.db_data) ######################################################################## class MainFrame(wx.Frame): #---------------------------------------------------------------------- def __init__(self): wx.Frame.__init__(self, parent=None, id=wx.ID_ANY, title="Database Viewer", size=(800,600)) panel = MainPanel(self) ######################################################################## class GenApp(wx.App): #---------------------------------------------------------------------- def __init__(self, redirect=False, filename=None): wx.App.__init__(self, redirect, filename) #---------------------------------------------------------------------- def OnInit(self): # create frame here frame = MainFrame() frame.Show() return True #---------------------------------------------------------------------- def main(): """ Run the demo """ app = GenApp() app.MainLoop() if __name__ == "__main__": main()
Let’s spend a little time breaking down how this works. You’ll note that after we create out ObjectListView widget, we hide it as we don’t yet know what’s going into it. That won’t happen until the user clicks the Load DB button. In the loadDatabase handler, we pop-up a file dialog to allow the user to select the SQLite database file they want to load. For this test, I would recommend using the database we created earlier. After that’s selected, we create an SQLALchemy engine, pull the table names from it, set the combobox drop-down to the list of tablenames and then load the first table in the list by calling our loadTable method.
In loadTable, we use SQLAlchemy’s handy autoload feature to “reflect” the data from the database into a Table object. We call clear_mappers
because we need to make sure that nothing is currently mapped to our dummy class and then we map our new table to the class. Finally we create a SQLAlchemy session and do a simple SELECT * query to pull all the records from the database and pass them to the ObjectListView widget, which we then show.
Wrapping Up
I tried running this script against Mozilla’s places database, but my little application didn’t like it. Feel free to try to break it with your databases. Currently this is only a proof-of-concept. I had thought of something similar to this myself a year or so ago and I may try to improve this a bit in the future. In the mean time, I wanted to post my first draft and see what kind of feedback I can get. I hope you enjoy it!
Note: Tested on Windows 7, wxPython 2.8.12.1 with Python 2.6.6
Further Reading
Source Code
I knew about the Firefox plugin. I wrote this article just to show one way you could do this with Python itself. I kind of want to expand it so it’s more full-featured like the ones you pointed out though.
Simon,
That’s cool! I thought it should work. Mozilla must be doing something funky with their sqlite database. I’ll have to take a look at it with the Firefox SQLite plugin to figure out why it’s behaving so poorly.
Is it possible to create an onclick event handler for each cell?
Pingback: Python 101: How to Grab Data from RottenTomatoes - The Mouse Vs. The Python