Welcome, guest | Sign In | My Account | Store | Cart
NOTE: Recipes have moved! Please visit GitHub.com/activestate/code for the current versions.

This is a very simple script to allow someone to examine a ZODB database. Start the script and you can see what is in your ZODB file as well as the overall structure.

Python, 333 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
"""wxView.py - a simple view for ZODB files

TODO:
    -Support ZEO
    -Rewrite/extend to use the builtin HTTP server a la pydoc
"""
import UserDict
import UserList
import locale
import os
import os.path
import sys

import wx

import ZODB
from   ZODB import FileStorage, DB
from persistent import Persistent
from BTrees.OOBTree import OOBTree
from persistent.list import PersistentList as PList
from persistent.mapping import PersistentMapping as PMap

def close_zodb(DataBase):
    """Closes the ZODB.

    This function MUST be called at the end of each program !!!
    See open_zodb() for a description of the argument.
    """
    get_transaction().abort()
    DataBase[1].close()
    DataBase[2].close()
    DataBase[3].close()
    return True

def open_zodb(Path):
    """Open ZODB.

    Returns a tuple consisting of:(root,connection,db,storage)
    The same tuple must be passed to close_zodb() in order to close the DB.
    """
    # Connect to DB
    storage     = FileStorage.FileStorage(Path)
    db          = DB(storage)
    connection  = db.open()
    root        = connection.root()
    return (root,connection,db,storage)

def save_pos(win, cfg):
    """Save a window position to the registry"""
    (xpos, ypos) = win.GetPositionTuple()
    (width, height) = win.GetSizeTuple()
    cfg.WriteInt('xpos', xpos)
    cfg.WriteInt('ypos', ypos)
    cfg.WriteInt('width', width)
    cfg.WriteInt('height', height)

def set_pos(win, cfg):
    """Restore a window to a position from the registry"""
    xpos = cfg.ReadInt('xpos', -1)
    ypos = cfg.ReadInt('ypos', -1)
    width = cfg.ReadInt('width', -1)
    height = cfg.ReadInt('height', -1)
    win.SetDimensions(xpos, ypos, width, height)

class ZODBFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        # begin wxGlade: ZODBFrame.__init__
        kwds["style"] = wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)
        self.window_1 = wx.SplitterWindow(self, -1,
                                          style=wx.SP_3D|wx.SP_BORDER)
        self.panel_1 = wx.Panel(self.window_1, -1)
        self.window_1_pane_1 = wx.Panel(self.window_1, -1)

        # Menu Bar
        self.mb = wx.MenuBar()
        self.SetMenuBar(self.mb)
        self.mnuFile = wx.Menu()
        self.mnuOpen = wx.MenuItem(self.mnuFile, wx.ID_OPEN, "&Open\tCtrl-O",
                                   "", wx.ITEM_NORMAL)
        self.mnuFile.AppendItem(self.mnuOpen)
        self.mnuFile.Append(wx.ID_CLOSE, "&Close", "", wx.ITEM_NORMAL)
        self.mnuFile.AppendSeparator()
        self.mnuFile.Append(wx.ID_EXIT, "E&xit", "", wx.ITEM_NORMAL)
        self.mb.Append(self.mnuFile, "&File")
        # Menu Bar end
        self.sb = self.CreateStatusBar(1, wx.ST_SIZEGRIP)
        self.db_layout_tree = wx.TreeCtrl(self.window_1_pane_1, -1,
                                          style=wx.TR_HAS_BUTTONS|
                                                wx.TR_LINES_AT_ROOT|
                                                wx.TR_DEFAULT_STYLE|
                                                wx.SUNKEN_BORDER)
        self.label_1 = wx.StaticText(self.panel_1, -1, "Data Type:")
        self.txtType = wx.StaticText(self.panel_1, -1, "txtType")
        self.txtData = wx.TextCtrl(self.panel_1, -1, "", style=wx.TE_MULTILINE)

        self.__set_properties()
        self.__do_layout()
        # end wxGlade

        self.__create_image_list()
        self.__create_file_history()
        self.__set_window_position()
        self.__set_bindings()

        self.db = None
        self.root = None

    def __create_file_history(self):
        self.file_history = wx.FileHistory()
        self.file_history.UseMenu(self.mnuFile)
        old_path = self.wxcfg.GetPath()
        self.wxcfg.SetPath('/RecentFiles')
        self.file_history.Load(self.wxcfg)
        self.wxcfg.SetPath(old_path)
        self._need_save = False

    def __create_image_list(self):
        """Setup our image list for the tree control"""
        isz = (16, 16)
        il = wx.ImageList(*isz)
        self.folder_idx = il.Add(wx.ArtProvider_GetBitmap(wx.ART_FOLDER,
                                wx.ART_OTHER, isz))
        self.folder_open_idx = il.Add(wx.ArtProvider_GetBitmap(wx.ART_FILE_OPEN,
                                wx.ART_OTHER, isz))
        self.file_idx = il.Add(wx.ArtProvider_GetBitmap(wx.ART_REPORT_VIEW,
                                wx.ART_OTHER, isz))
        self.il = il

    def __set_bindings(self):
        self.Bind(wx.EVT_CLOSE, self.onExit)
        self.Bind(wx.EVT_MENU, self.onExit, id=wx.ID_EXIT)
        self.Bind(wx.EVT_MENU, self.doOpen, id=wx.ID_OPEN)
        self.Bind(wx.EVT_MENU, self.doClose, id=wx.ID_CLOSE)
        self.Bind(wx.EVT_MENU_RANGE, self.doFileHistory, id=wx.ID_FILE1,
                  id2=wx.ID_FILE9)

        self.db_layout_tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.onSelChange)

    def __set_properties(self):
        # begin wxGlade: ZODBFrame.__set_properties
        self.SetTitle("ZODB Viewer")
        self.sb.SetStatusWidths([-1])
        # statusbar fields
        sb_fields = [""]
        for i in range(len(sb_fields)):
            self.sb.SetStatusText(sb_fields[i], i)
        self.txtType.SetFont(wx.Font(12, wx.MODERN, wx.NORMAL, wx.BOLD, 0,
                                     "Courier New"))
        self.txtData.SetFont(wx.Font(12, wx.MODERN, wx.NORMAL, wx.NORMAL, 0,
                                     "Courier New"))
        # end wxGlade

    def __set_window_position(self):
        self.wxcfg = wx.Config()
        old_path = self.wxcfg.GetPath()
        self.wxcfg.SetPath('/Window Information')
        set_pos(self, self.wxcfg)
        self.wxcfg.SetPath(old_path)

    def __do_layout(self):
        # begin wxGlade: ZODBFrame.__do_layout
        sizer_1 = wx.BoxSizer(wx.HORIZONTAL)
        grid_sizer_2 = wx.FlexGridSizer(2, 1, 5, 0)
        grid_sizer_3 = wx.FlexGridSizer(1, 2, 0, 5)
        sizer_2 = wx.BoxSizer(wx.HORIZONTAL)
        sizer_2.Add(self.db_layout_tree, 1, wx.EXPAND, 0)
        self.window_1_pane_1.SetAutoLayout(True)
        self.window_1_pane_1.SetSizer(sizer_2)
        sizer_2.Fit(self.window_1_pane_1)
        sizer_2.SetSizeHints(self.window_1_pane_1)
        grid_sizer_3.Add(self.label_1, 0, wx.FIXED_MINSIZE, 0)
        grid_sizer_3.Add(self.txtType, 0, wx.FIXED_MINSIZE, 0)
        grid_sizer_3.AddGrowableCol(1)
        grid_sizer_2.Add(grid_sizer_3, 1, wx.EXPAND, 0)
        grid_sizer_2.Add(self.txtData, 0, wx.EXPAND|wx.FIXED_MINSIZE, 0)
        self.panel_1.SetAutoLayout(True)
        self.panel_1.SetSizer(grid_sizer_2)
        grid_sizer_2.Fit(self.panel_1)
        grid_sizer_2.SetSizeHints(self.panel_1)
        grid_sizer_2.AddGrowableRow(1)
        grid_sizer_2.AddGrowableCol(0)
        self.window_1.SplitVertically(self.window_1_pane_1, self.panel_1)
        sizer_1.Add(self.window_1, 1, wx.EXPAND, 0)
        self.SetAutoLayout(True)
        self.SetSizer(sizer_1)
        sizer_1.Fit(self)
        sizer_1.SetSizeHints(self)
        self.Layout()
        # end wxGlade

    def _set_child_icons(self, c, d):
        """Set the appropriate icons for a given tree child

        c
            The child we are updating

        d
            The data associated with the child
        """
        if isinstance(d, (dict, UserDict.UserDict, OOBTree)):
            self.db_layout_tree.SetItemImage(c,
                                             self.folder_idx,
                                             wx.TreeItemIcon_Normal)
            self.db_layout_tree.SetItemImage(c,
                                             self.folder_open_idx,
                                             wx.TreeItemIcon_Expanded)
        else:
            self.db_layout_tree.SetItemImage(c, self.file_idx,
                                                 wx.TreeItemIcon_Normal)

    def createTree(self, filename):
        """Create a new tree structure for when we open a file"""
        self.doClose()

        self.db = open_zodb(filename)
        self.db_layout_tree.SetImageList(self.il)

        self.root = self.db_layout_tree.AddRoot(os.path.basename(filename))
        self.db_layout_tree.SetPyData(self.root, self.db[0])
        self.db_layout_tree.SetItemImage(self.root, self.folder_idx,
                                         wx.TreeItemIcon_Normal)
        self.db_layout_tree.SetItemImage(self.root, self.folder_open_idx,
                                         wx.TreeItemIcon_Expanded)

        db = self.db[0]
        for key in db.keys():
            child = self.db_layout_tree.AppendItem(self.root, key)
            self.db_layout_tree.SetPyData(child, db[key])
            if isinstance(db[key], dict) or isinstance(db[key], list):
                self.db_layout_tree.SetItemImage(child, self.folder_idx,
                                                 wx.TreeItemIcon_Normal)
                self.db_layout_tree.SetItemImage(child, self.folder_open_idx,
                                                 wx.TreeItemIcon_Expanded)
            else:
                self.db_layout_tree.SetItemImage(child, self.file_idx,
                                                 wx.TreeItemIcon_Normal)

        self.db_layout_tree.Expand(self.root)

    def doClose(self, *event):
        """Close the current file and clear the screen"""
        if self.db:
            close_zodb(self.db)
            self.db = None
        if self.root:
            self.db_layout_tree.DeleteAllItems()
        self.txtType.SetLabel('')
        self.txtData.Clear()

    def doFileHistory(self, event):
        """Open a file from file history"""
        file_number = event.GetId() - wx.ID_FILE1
        filename = self.file_history.GetHistoryFile(file_number)
        self.createTree(filename)

    def doOpen(self, *event):
        """Open a file from the file system"""
        # Select and open the ZODB file object.
        dlg = wx.FileDialog(self, message="Choose a file",
            defaultFile="", style=wx.OPEN | wx.CHANGE_DIR)

        if dlg.ShowModal() == wx.ID_OK:
            # This returns a Python list of files that were selected.
            filename = dlg.GetPath()
            self.createTree(filename)
            self.file_history.AddFileToHistory(filename)
        dlg.Destroy()

    def onExit(self, event):
        """Exit the program"""
        self.doClose()

        old_path = self.wxcfg.GetPath()
        self.wxcfg.SetPath('/RecentFiles')
        self.file_history.Save(self.wxcfg)
        self.wxcfg.SetPath(old_path)

        old_path = self.wxcfg.GetPath()
        self.wxcfg.SetPath('/Window Information')
        save_pos(self, self.wxcfg)
        self.wxcfg.SetPath(old_path)
        self.Destroy()

    def onSelChange(self, event):
        """Select a new tree node, loading it if needed"""
        item = event.GetItem()
        data = self.db_layout_tree.GetPyData(item)
        if isinstance(data, (dict, UserDict.UserDict, OOBTree)):
            self.txtData.Clear()
            self.txtType.SetLabel(str(type(data)))
            if hasattr(data, 'wx_str'):
                self.txtData.AppendText(data.wx_str())
            if not self.db_layout_tree.ItemHasChildren(item):
                keys = data.keys()
                try:
                    keys.sort()
                except AttributeError:
                    pass
                for key in keys:
                    child = self.db_layout_tree.AppendItem(item, str(key))
                    self.db_layout_tree.SetPyData(child, data[key])
                    self._set_child_icons(child, data[key])
        elif isinstance(data, (list, UserList.UserList)):
            self.txtData.Clear()
            self.txtType.SetLabel(str(type(data)))
            for d in data:
                self.txtData.AppendText(str(type(d)) + ' --\n')
                if hasattr(d, 'wx_str'):
                    self.txtData.AppendText(d.wx_str())
                else:
                    self.txtData.AppendText(str(d))
                self.txtData.AppendText('\n')
        else:
            self.txtType.SetLabel(str(type(data)))
            fmt = '%s\n-----\n%s'
            if hasattr(data, 'wx_str'):
                self.txtData.SetValue(data.wx_str())
            else:
                self.txtData.SetValue(str(data))

# end of class ZODBFrame


if __name__ == "__main__":
    locale.setlocale(locale.LC_ALL, '')

    wxviewdb = wx.PySimpleApp(0)
    wx.InitAllImageHandlers()
    frmZODB = ZODBFrame(None, -1, "")
    wxviewdb.SetTopWindow(frmZODB)
    frmZODB.Show()
    wxviewdb.MainLoop()

I wrote this because I've done a bad job of documenting my database. The code is fine, however when I needed to go back and extract data in a different way I sometimes was having problems figuring out what I did. Worse, for me, is that I now have someone else helping on the codebase and they needed a simple way to see how the data was laid out.

When the data is displayed the script first looks for a method called 'wx_str'. This method should return a text representation of the object. I chose this instead of using __str__ since I use __str__ for other things and I am considering allowing wx_str to return a more complex data representation for fancier output. Obviously, if wx_str isn't available __str__ is used.

On the todo side I think allowing other connection methods (say ZEO) would be a good addition, as well as making this a bit more robust. We'll see what time permits. :)

4/8/2005: Updated with a slight change in formatting to get line length down to 79 chars

9 comments

Tyler Bye 12 years, 9 months ago  # | flag

Great contribution! Slight startup error.

I tried to run this out of the box and was not able to get it to fire.

Here's the trace I got.

Traceback (most recent call last):
  File "zodbviewer.py", line 330, in ?
    frmZODB = ZODBFrame(None, -1, "")
  File "zodbviewer.py", line 102, in __init__
    self.__create_file_history()
  File "zodbviewer.py", line 112, in __create_file_history
    old_path = self.wxcfg.GetPath()
AttributeError: 'ZODBFrame' object has no attribute 'wxcfg'

By commenting out line #105

...
self.__create_file_history()
...

I was able to run it okay.  I did have to force it closed.
Duncan McGreggor 12 years, 9 months ago  # | flag

Similar issue, different solution. I had the same issue. I added self.wxcfg = wx.Config() to __init__(), and was able to use it. <p></p> Also, a few things to note:

This uses the latest persistent.Persistent from Zope3 instead of the old Persistence.Persistent.

You will need to import your Persistent subclasses (the ones you defined in which to store your data) in order for them to be "opened" in the tree by this script.

Duncan McGreggor 12 years, 9 months ago  # | flag

ZEO Support. I've hacked in ZEO support -- was pretty straight forward. Added a new menu item, did an import of ZEO.ClientStorage, added a few checks, etc. Very cool, very useful little tool -- thanks for sharing!

Peter Arwanitis 12 years, 8 months ago  # | flag

Would you share your ZEO addon? Hi Duncan,

would be nice to see your zeo-addon as a recontribution :)

is this possible?

thanks Peter

Duncan McGreggor 12 years, 8 months ago  # | flag

ZEO Hack. Sure... let me find it...

Okay, you'll need this import:

from ZEO import ClientStorage

I then modified the open_zodb() function:

def open_zodb(Path, zeo=False):
    """Open ZODB.

    Returns a tuple consisting of:(root,connection,db,storage)
    The same tuple must be passed to close_zodb() in order to close the DB.
    """
    # Connect to DB
    if zeo:
        server = Path.split(':')
        server[1] = int(server[1])
        storage = ClientStorage.ClientStorage(tuple(server))
    else:
        storage = FileStorage.FileStorage(Path)
    db          = DB(storage)
    connection  = db.open()
    root        = connection.root()
    return (root,connection,db,storage)

To ZODBFrame.__init__(), I added this:

   self.mnuFile.AppendItem(self.mnuOpen)
+  self.mnuOpenZEO = wx.MenuItem(self.mnuFile, wx.ID_OK, "&Open ZEO Instance...\tCtrl-U",
       "", wx.ITEM_NORMAL)
+  self.mnuFile.AppendItem(self.mnuOpenZEO)

I modified ZODBFrame.creatTree() to include this:

def createTree(self, filename='', server='', zeo=False):
    """Create a new tree structure for when we open a file"""
    self.doClose()

    if zeo:
        self.db = open_zodb(server, zeo=True)
        self.root = self.db_layout_tree.AddRoot(server)
    else:
        self.db = open_zodb(filename)
        self.root = self.db_layout_tree.AddRoot(os.path.basename(filename))

And, finally, I added this method:

def doOpenUrl(self, *event):
    """Access a ZEO instance from a url"""
    dlg = wx.TextEntryDialog(self, message="Enter the server and port for the ZEO instance (hostname_or_ip:port)")
    if dlg.ShowModal() == wx.ID_OK:
        self.createTree(zeo=True, server=dlg.GetValue())
    dlg.Destroy()

I may have midded something... and this was done against the first version of the recipe...

Imre Takacs 12 years, 6 months ago  # | flag

I think, there should be another line. It is a great program. I think, there should be a line in the _set_binding() method:

self.Bind(wx, EVT_MENU, self.doOpenUrl, id=wx.ID_OK

Win Myint Aung 10 years, 11 months ago  # | flag

Exporting wiki contents from ZODB to MYSQL. Please do me a favor. I am looking for a python script that can export wiki contents which is stored in ZODB to MYSQL.

Thanks

Vanjeer 9 years, 2 months ago  # | flag

As Duncan said 1. self.wxcfg = wx.Config() to __init__()

and, then

  1. add import transaction
  2. find get_transaction() and replace with transaction - (no braces in transaction)

The crashing is due to a new module name change in ZODB 3.6

Blind One 8 years, 7 months ago  # | flag

Hi, i've download your recipe and try to run it but have several errors on traceback: File "C:/Program Files/Python 2.6.2/recipe-409012-2.py", line 14, in <module> import zodb ImportError: No module named zodb.

Please, tell me, where I can get this module? I've used a zodb docflow system and try to connect to it's database. Python 2.6.2 and wxPython 2.8 for Python 2.6 also installed.