Code

Did a fair bit of work on the admin tool. Now has an extra command "table"
[roundup.git] / roundup-admin
1 #! /Users/builder/bin/python
2 #
3 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
4 # This module is free software, and you may redistribute it and/or modify
5 # under the same terms as Python, so long as this copyright message and
6 # disclaimer are retained in their original form.
7 #
8 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
9 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
10 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
11 # POSSIBILITY OF SUCH DAMAGE.
12 #
13 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
14 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
15 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
16 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
17 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
18
19 # $Id: roundup-admin,v 1.33 2001-10-17 23:13:19 richard Exp $
21 import sys
22 if int(sys.version[0]) < 2:
23     print 'Roundup requires python 2.0 or later.'
24     sys.exit(1)
26 import string, os, getpass, getopt, re
27 try:
28     import csv
29 except ImportError:
30     csv = None
31 from roundup import date, hyperdb, roundupdb, init, password
32 import roundup.instance
34 def usage(message=''):
35     if message: message = 'Problem: '+message+'\n'
36     print '''%sUsage: roundup-admin [-i instance home] [-u login] [-c] <command> <arguments>
38 Help:
39  roundup-admin -h
40  roundup-admin help                       -- this help
41  roundup-admin help <command>             -- command-specific help
42  roundup-admin help all                   -- all available help
43 Options:
44  -i instance home  -- specify the issue tracker "home directory" to administer
45  -u                -- the user[:password] to use for commands
46  -c                -- when outputting lists of data, just comma-separate them'''%message
47     help_commands()
49 def help_commands():
50     print 'Commands:',
51     commands = ['']
52     for command in figureCommands().values():
53         h = command.__doc__.split('\n')[0]
54         commands.append(h[7:])
55     commands.sort()
56     print '\n '.join(commands)
58 def help_all():
59     print '''
60 All commands (except help) require an instance specifier. This is just the path
61 to the roundup instance you're working with. A roundup instance is where 
62 roundup keeps the database and configuration file that defines an issue
63 tracker. It may be thought of as the issue tracker's "home directory". It may
64 be specified in the environment variable ROUNDUP_INSTANCE or on the command
65 line as "-i instance".
67 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
69 Property values are represented as strings in command arguments and in the
70 printed results:
71  . Strings are, well, strings.
72  . Date values are printed in the full date format in the local time zone, and
73    accepted in the full format or any of the partial formats explained below.
74  . Link values are printed as node designators. When given as an argument,
75    node designators and key strings are both accepted.
76  . Multilink values are printed as lists of node designators joined by commas.
77    When given as an argument, node designators and key strings are both
78    accepted; an empty string, a single node, or a list of nodes joined by
79    commas is accepted.
81 When multiple nodes are specified to the roundup get or roundup set
82 commands, the specified properties are retrieved or set on all the listed
83 nodes. 
85 When multiple results are returned by the roundup get or roundup find
86 commands, they are printed one per line (default) or joined by commas (with
87 the -c) option. 
89 Where the command changes data, a login name/password is required. The
90 login may be specified as either "name" or "name:password".
91  . ROUNDUP_LOGIN environment variable
92  . the -u command-line option
93 If either the name or password is not supplied, they are obtained from the
94 command-line. 
96 Date format examples:
97   "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
98   "2000-04-17" means <Date 2000-04-17.00:00:00>
99   "01-25" means <Date yyyy-01-25.00:00:00>
100   "08-13.22:13" means <Date yyyy-08-14.03:13:00>
101   "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
102   "14:25" means <Date yyyy-mm-dd.19:25:00>
103   "8:47:11" means <Date yyyy-mm-dd.13:47:11>
104   "." means "right now"
106 Command help:
107 '''
108     for name, command in figureCommands().items():
109         print '%s:'%name
110         print '   ',command.__doc__
112 def do_help(args):
113     '''Usage: help topic
114     Give help about topic.
116     commands  -- list commands
117     <command> -- help specific to a command
118     initopts  -- init command options
119     all       -- all available help
120     '''
121     help = figureHelp().get(args[0], None)
122     if help:
123         help()
124         return
125     help = figureCommands().get(args[0], None)
126     if help:
127         print help.__doc__
128     else:
129         print 'Sorry, no help for "%s"'%args[0]
131 def help_initopts():
132     import roundup.templates
133     templates = roundup.templates.listTemplates()
134     print 'Templates:', ', '.join(templates)
135     import roundup.backends
136     backends = roundup.backends.__all__
137     print 'Back ends:', ', '.join(backends)
140 def do_init(instance_home, args, comma_sep=0):
141     '''Usage: init [template [backend [admin password]]]
142     Initialise a new Roundup instance.
144     The command will prompt for the instance home directory (if not supplied
145     through INSTANCE_HOME or the -i option. The template, backend and admin
146     password may be specified on the command-line as arguments, in that order.
148     See also initopts help.
149     '''
150     # select template
151     import roundup.templates
152     templates = roundup.templates.listTemplates()
153     template = len(args) > 1 and args[1] or ''
154     if template not in templates:
155         print 'Templates:', ', '.join(templates)
156     while template not in templates:
157         template = raw_input('Select template [extended]: ').strip()
158         if not template:
159             template = 'extended'
161     import roundup.backends
162     backends = roundup.backends.__all__
163     backend = len(args) > 2 and args[2] or ''
164     if backend not in backends:
165         print 'Back ends:', ', '.join(backends)
166     while backend not in backends:
167         backend = raw_input('Select backend [anydbm]: ').strip()
168         if not backend:
169             backend = 'anydbm'
170     if len(args) > 3:
171         adminpw = confirm = args[3]
172     else:
173         adminpw = ''
174         confirm = 'x'
175     while adminpw != confirm:
176         adminpw = getpass.getpass('Admin Password: ')
177         confirm = getpass.getpass('       Confirm: ')
178     init.init(instance_home, template, backend, adminpw)
179     return 0
182 def do_get(db, args, comma_sep=0):
183     '''Usage: get property designator[,designator]*
184     Get the given property of one or more designator(s).
186     Retrieves the property value of the nodes specified by the designators.
187     '''
188     propname = args[0]
189     designators = string.split(args[1], ',')
190     l = []
191     for designator in designators:
192         classname, nodeid = roundupdb.splitDesignator(designator)
193         if comma_sep:
194             l.append(db.getclass(classname).get(nodeid, propname))
195         else:
196             print db.getclass(classname).get(nodeid, propname)
197     if comma_sep:
198         print ','.join(l)
199     return 0
202 def do_set(db, args, comma_sep=0):
203     '''Usage: set designator[,designator]* propname=value ...
204     Set the given property of one or more designator(s).
206     Sets the property to the value for all designators given.
207     '''
208     from roundup import hyperdb
210     designators = string.split(args[0], ',')
211     props = {}
212     for prop in args[1:]:
213         key, value = prop.split('=')
214         props[key] = value
215     for designator in designators:
216         classname, nodeid = roundupdb.splitDesignator(designator)
217         cl = db.getclass(classname)
218         properties = cl.getprops()
219         for key, value in props.items():
220             type =  properties[key]
221             if isinstance(type, hyperdb.String):
222                 continue
223             elif isinstance(type, hyperdb.Password):
224                 props[key] = password.Password(value)
225             elif isinstance(type, hyperdb.Date):
226                 props[key] = date.Date(value)
227             elif isinstance(type, hyperdb.Interval):
228                 props[key] = date.Interval(value)
229             elif isinstance(type, hyperdb.Link):
230                 props[key] = value
231             elif isinstance(type, hyperdb.Multilink):
232                 props[key] = value.split(',')
233         apply(cl.set, (nodeid, ), props)
234     return 0
236 def do_find(db, args, comma_sep=0):
237     '''Usage: find classname propname=value ...
238     Find the nodes of the given class with a given link property value.
240     Find the nodes of the given class with a given link property value. The
241     value may be either the nodeid of the linked node, or its key value.
242     '''
243     classname = args[0]
244     cl = db.getclass(classname)
246     # look up the linked-to class and get the nodeid that has the value
247     propname, value = args[1].split('=')
248     num_re = re.compile('^\d+$')
249     if not num_re.match(value):
250         propcl = cl.properties[propname]
251         if (not isinstance(propcl, hyperdb.Link) and not
252                 isinstance(type, hyperdb.Multilink)):
253             print 'You may only "find" link properties'
254             return 1
255         propcl = db.getclass(propcl.classname)
256         value = propcl.lookup(value)
258     # now do the find
259     if comma_sep:
260         print ','.join(cl.find(**{propname: value}))
261     else:
262         print cl.find(**{propname: value})
263     return 0
265 def do_spec(db, args, comma_sep=0):
266     '''Usage: spec classname
267     Show the properties for a classname.
269     This lists the properties for a given class.
270     '''
271     classname = args[0]
272     cl = db.getclass(classname)
273     keyprop = cl.getkey()
274     for key, value in cl.properties.items():
275         if keyprop == key:
276             print '%s: %s (key property)'%(key, value)
277         else:
278             print '%s: %s'%(key, value)
280 def do_create(db, args, comma_sep=0):
281     '''Usage: create classname property=value ...
282     Create a new entry of a given class.
284     This creates a new entry of the given class using the property
285     name=value arguments provided on the command line after the "create"
286     command.
287     '''
288     from roundup import hyperdb
290     classname = args[0]
291     cl = db.getclass(classname)
292     props = {}
293     properties = cl.getprops(protected = 0)
294     if len(args) == 1:
295         # ask for the properties
296         for key, value in properties.items():
297             if key == 'id': continue
298             name = value.__class__.__name__
299             if isinstance(value , hyperdb.Password):
300                 again = None
301                 while value != again:
302                     value = getpass.getpass('%s (Password): '%key.capitalize())
303                     again = getpass.getpass('   %s (Again): '%key.capitalize())
304                     if value != again: print 'Sorry, try again...'
305                 if value:
306                     props[key] = value
307             else:
308                 value = raw_input('%s (%s): '%(key.capitalize(), name))
309                 if value:
310                     props[key] = value
311     else:
312         # use the args
313         for prop in args[1:]:
314             key, value = prop.split('=')
315             props[key] = value 
317     # convert types
318     for key in props.keys():
319         type =  properties[key]
320         if isinstance(type, hyperdb.Date):
321             props[key] = date.Date(value)
322         elif isinstance(type, hyperdb.Interval):
323             props[key] = date.Interval(value)
324         elif isinstance(type, hyperdb.Password):
325             props[key] = password.Password(value)
326         elif isinstance(type, hyperdb.Multilink):
327             props[key] = value.split(',')
329     if cl.getkey() and not props.has_key(cl.getkey()):
330         print "You must provide the '%s' property."%cl.getkey()
331     else:
332         print apply(cl.create, (), props)
334     return 0
336 def do_list(db, args, comma_sep=0):
337     '''Usage: list classname [property]
338     List the instances of a class.
340     Lists all instances of the given class. If the property is not
341     specified, the  "label" property is used. The label property is tried
342     in order: the key, "name", "title" and then the first property,
343     alphabetically.
344     '''
345     classname = args[0]
346     cl = db.getclass(classname)
347     if len(args) > 1:
348         key = args[1]
349     else:
350         key = cl.labelprop()
351     if comma_sep:
352         print ','.join(cl.list())
353     else:
354         for nodeid in cl.list():
355             value = cl.get(nodeid, key)
356             print "%4s: %s"%(nodeid, value)
357     return 0
359 def do_table(db, args, comma_sep=None):
360     '''Usage: table classname [property[,property]*]
361     List the instances of a class in tabular form.
363     Lists all instances of the given class. If the properties are not
364     specified, all properties are displayed. By default, the column widths
365     are the width of the property names. The width may be explicitly defined
366     by defining the property as "name:width". For example::
367       roundup> table priority id,name:10
368       Id Name
369       1  fatal-bug 
370       2  bug       
371       3  usability 
372       4  feature   
373     '''
374     classname = args[0]
375     cl = db.getclass(classname)
376     if len(args) > 1:
377         prop_names = args[1].split(',')
378     else:
379         prop_names = cl.getprops().keys()
380     props = []
381     for name in prop_names:
382         if ':' in name:
383             name, width = name.split(':')
384             props.append((name, int(width)))
385         else:
386             props.append((name, len(name)))
388     print ' '.join([string.capitalize(name) for name, width in props])
389     for nodeid in cl.list():
390         l = []
391         for name, width in props:
392             if name != 'id':
393                 value = str(cl.get(nodeid, name))
394             else:
395                 value = str(nodeid)
396             f = '%%-%ds'%width
397             l.append(f%value[:width])
398         print ' '.join(l)
399     return 0
401 def do_history(db, args, comma_sep=0):
402     '''Usage: history designator
403     Show the history entries of a designator.
405     Lists the journal entries for the node identified by the designator.
406     '''
407     classname, nodeid = roundupdb.splitDesignator(args[0])
408     # TODO: handle the -c option?
409     print db.getclass(classname).history(nodeid)
410     return 0
412 def do_retire(db, args, comma_sep=0):
413     '''Usage: retire designator[,designator]*
414     Retire the node specified by designator.
416     This action indicates that a particular node is not to be retrieved by
417     the list or find commands, and its key value may be re-used.
418     '''
419     designators = string.split(args[0], ',')
420     for designator in designators:
421         classname, nodeid = roundupdb.splitDesignator(designator)
422         db.getclass(classname).retire(nodeid)
423     return 0
425 def do_export(db, args, comma_sep=0):
426     '''Usage: export class[,class] destination_dir
427     Export the database to tab-separated-value files.
429     This action exports the current data from the database into
430     tab-separated-value files that are placed in the nominated destination
431     directory. The journals are not exported.
432     '''
433     if len(args) < 2:
434         print do_export.__doc__
435         return 1
436     classes = string.split(args[0], ',')
437     dir = args[1]
439     # use the csv parser if we can - it's faster
440     if csv is not None:
441         p = csv.parser(field_sep=':')
443     # do all the classes specified
444     for classname in classes:
445         cl = db.getclass(classname)
446         f = open(os.path.join(dir, classname+'.csv'), 'w')
447         f.write(string.join(cl.properties.keys(), ':') + '\n')
449         # all nodes for this class
450         properties = cl.properties.items()
451         for nodeid in cl.list():
452             l = []
453             for prop, type in properties:
454                 value = cl.get(nodeid, prop)
455                 # convert data where needed
456                 if isinstance(type, hyperdb.Date):
457                     value = value.get_tuple()
458                 elif isinstance(type, hyperdb.Interval):
459                     value = value.get_tuple()
460                 elif isinstance(type, hyperdb.Password):
461                     value = str(value)
462                 l.append(repr(value))
464                         # now write
465             if csv is not None:
466                f.write(p.join(l) + '\n')
467             else:
468                # escape the individual entries to they're valid CSV
469                m = []
470                for entry in l:
471                   if '"' in entry:
472                       entry = '""'.join(entry.split('"'))
473                   if ':' in entry:
474                       entry = '"%s"'%entry
475                   m.append(entry)
476                f.write(':'.join(m) + '\n')
477     return 0
479 def do_import(db, args, comma_sep=0):
480     '''Usage: import class file
481     Import the contents of the tab-separated-value file.
483     The file must define the same properties as the class (including having
484     a "header" line with those property names.) The new nodes are added to
485     the existing database - if you want to create a new database using the
486     imported data, then create a new database (or, tediously, retire all
487     the old data.)
488     '''
489     if len(args) < 2:
490         print do_import.__doc__
491         return 1
492     if csv is None:
493         print 'Sorry, you need the csv module to use this function.'
494         print 'Get it from: http://www.object-craft.com.au/projects/csv/'
495         return 1
497     from roundup import hyperdb
499     # ensure that the properties and the CSV file headings match
500     cl = db.getclass(args[0])
501     f = open(args[1])
502     p = csv.parser(field_sep=':')
503     file_props = p.parse(f.readline())
504     props = cl.properties.keys()
505     m = file_props[:]
506     m.sort()
507     props.sort()
508     if m != props:
509         print 'Import file doesn\'t define the same properties as "%s".'%args[0]
510         return 1
512     # loop through the file and create a node for each entry
513     n = range(len(props))
514     while 1:
515         line = f.readline()
516         if not line: break
518         # parse lines until we get a complete entry
519         while 1:
520             l = p.parse(line)
521             if l: break
523         # make the new node's property map
524         d = {}
525         for i in n:
526             # Use eval to reverse the repr() used to output the CSV
527             value = eval(l[i])
528             # Figure the property for this column
529             key = file_props[i]
530             type = cl.properties[key]
531             # Convert for property type
532             if isinstance(type, hyperdb.Date):
533                 value = date.Date(value)
534             elif isinstance(type, hyperdb.Interval):
535                 value = date.Interval(value)
536             elif isinstance(type, hyperdb.Password):
537                 pwd = password.Password()
538                 pwd.unpack(value)
539                 value = pwd
540             if value is not None:
541                 d[key] = value
543         # and create the new node
544         apply(cl.create, (), d)
545     return 0
547 def figureCommands():
548     d = {}
549     for k, v in globals().items():
550         if k[:3] == 'do_':
551             d[k[3:]] = v
552     return d
554 def figureHelp():
555     d = {}
556     for k, v in globals().items():
557         if k[:5] == 'help_':
558             d[k[5:]] = v
559     return d
561 class AdminTool:
562     def run_command(self, args):
563         '''Run a single command
564         '''
565         command = args[0]
567         # handle help now
568         if command == 'help':
569             if len(args)>1:
570                 do_help(args[1:])
571                 return 0
572             do_help(['help'])
573             return 0
574         if command == 'morehelp':
575             do_help(['help'])
576             help_commands()
577             help_all()
578             return 0
580         # make sure we have an instance_home
581         while not self.instance_home:
582             self.instance_home = raw_input('Enter instance home: ').strip()
584         # before we open the db, we may be doing an init
585         if command == 'init':
586             return do_init(self.instance_home, args)
588         function = figureCommands().get(command, None)
590         # not a valid command
591         if function is None:
592             print 'Unknown command "%s" ("help commands" for a list)'%command
593             return 1
595         # get the instance
596         instance = roundup.instance.open(self.instance_home)
597         db = instance.open('admin')
599         if len(args) < 2:
600             print function.__doc__
601             return 1
603         # do the command
604         try:
605             return function(db, args[1:], comma_sep=self.comma_sep)
606         finally:
607             db.close()
609         return 1
611     def interactive(self, ws_re=re.compile(r'\s+')):
612         '''Run in an interactive mode
613         '''
614         print 'Roundup {version} ready for input.'
615         print 'Type "help" for help.'
616         try:
617             import readline
618         except ImportError:
619             print "Note: command history and editing not available"
621         while 1:
622             try:
623                 command = raw_input('roundup> ')
624             except EOFError:
625                 print '.. exit'
626                 return 0
627             args = ws_re.split(command)
628             if not args: continue
629             if args[0] in ('quit', 'exit'): return 0
630             self.run_command(args)
632     def main(self):
633         opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
635         # handle command-line args
636         self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
637         name = password = ''
638         if os.environ.has_key('ROUNDUP_LOGIN'):
639             l = os.environ['ROUNDUP_LOGIN'].split(':')
640             name = l[0]
641             if len(l) > 1:
642                 password = l[1]
643         self.comma_sep = 0
644         for opt, arg in opts:
645             if opt == '-h':
646                 usage()
647                 return 0
648             if opt == '-i':
649                 self.instance_home = arg
650             if opt == '-c':
651                 self.comma_sep = 1
653         # if no command - go interactive
654         if not args:
655             return self.interactive()
657         self.run_command(args)
660 if __name__ == '__main__':
661     tool = AdminTool()
662     sys.exit(tool.main())
665 # $Log: not supported by cvs2svn $
666 # Revision 1.32  2001/10/17 06:57:29  richard
667 # Interactive startup blurb - need to figure how to get the version in there.
669 # Revision 1.31  2001/10/17 06:17:26  richard
670 # Now with readline support :)
672 # Revision 1.30  2001/10/17 06:04:00  richard
673 # Beginnings of an interactive mode for roundup-admin
675 # Revision 1.29  2001/10/16 03:48:01  richard
676 # admin tool now complains if a "find" is attempted with a non-link property.
678 # Revision 1.28  2001/10/13 00:07:39  richard
679 # More help in admin tool.
681 # Revision 1.27  2001/10/11 23:43:04  richard
682 # Implemented the comma-separated printing option in the admin tool.
683 # Fixed a typo (more of a vim-o actually :) in mailgw.
685 # Revision 1.26  2001/10/11 05:03:51  richard
686 # Marked the roundup-admin import/export as experimental since they're not fully
687 # operational.
689 # Revision 1.25  2001/10/10 04:12:32  richard
690 # The setup.cfg file is just causing pain. Away it goes.
692 # Revision 1.24  2001/10/10 03:54:57  richard
693 # Added database importing and exporting through CSV files.
694 # Uses the csv module from object-craft for exporting if it's available.
695 # Requires the csv module for importing.
697 # Revision 1.23  2001/10/09 23:36:25  richard
698 # Spit out command help if roundup-admin command doesn't get an argument.
700 # Revision 1.22  2001/10/09 07:25:59  richard
701 # Added the Password property type. See "pydoc roundup.password" for
702 # implementation details. Have updated some of the documentation too.
704 # Revision 1.21  2001/10/05 02:23:24  richard
705 #  . roundup-admin create now prompts for property info if none is supplied
706 #    on the command-line.
707 #  . hyperdb Class getprops() method may now return only the mutable
708 #    properties.
709 #  . Login now uses cookies, which makes it a whole lot more flexible. We can
710 #    now support anonymous user access (read-only, unless there's an
711 #    "anonymous" user, in which case write access is permitted). Login
712 #    handling has been moved into cgi_client.Client.main()
713 #  . The "extended" schema is now the default in roundup init.
714 #  . The schemas have had their page headings modified to cope with the new
715 #    login handling. Existing installations should copy the interfaces.py
716 #    file from the roundup lib directory to their instance home.
717 #  . Incorrectly had a Bizar Software copyright on the cgitb.py module from
718 #    Ping - has been removed.
719 #  . Fixed a whole bunch of places in the CGI interface where we should have
720 #    been returning Not Found instead of throwing an exception.
721 #  . Fixed a deviation from the spec: trying to modify the 'id' property of
722 #    an item now throws an exception.
724 # Revision 1.20  2001/10/04 02:12:42  richard
725 # Added nicer command-line item adding: passing no arguments will enter an
726 # interactive more which asks for each property in turn. While I was at it, I
727 # fixed an implementation problem WRT the spec - I wasn't raising a
728 # ValueError if the key property was missing from a create(). Also added a
729 # protected=boolean argument to getprops() so we can list only the mutable
730 # properties (defaults to yes, which lists the immutables).
732 # Revision 1.19  2001/10/01 06:40:43  richard
733 # made do_get have the args in the correct order
735 # Revision 1.18  2001/09/18 22:58:37  richard
737 # Added some more help to roundu-admin
739 # Revision 1.17  2001/08/28 05:58:33  anthonybaxter
740 # added missing 'import' statements.
742 # Revision 1.16  2001/08/12 06:32:36  richard
743 # using isinstance(blah, Foo) now instead of isFooType
745 # Revision 1.15  2001/08/07 00:24:42  richard
746 # stupid typo
748 # Revision 1.14  2001/08/07 00:15:51  richard
749 # Added the copyright/license notice to (nearly) all files at request of
750 # Bizar Software.
752 # Revision 1.13  2001/08/05 07:44:13  richard
753 # Instances are now opened by a special function that generates a unique
754 # module name for the instances on import time.
756 # Revision 1.12  2001/08/03 01:28:33  richard
757 # Used the much nicer load_package, pointed out by Steve Majewski.
759 # Revision 1.11  2001/08/03 00:59:34  richard
760 # Instance import now imports the instance using imp.load_module so that
761 # we can have instance homes of "roundup" or other existing python package
762 # names.
764 # Revision 1.10  2001/07/30 08:12:17  richard
765 # Added time logging and file uploading to the templates.
767 # Revision 1.9  2001/07/30 03:52:55  richard
768 # init help now lists templates and backends
770 # Revision 1.8  2001/07/30 02:37:07  richard
771 # Freshen is really broken. Commented out.
773 # Revision 1.7  2001/07/30 01:28:46  richard
774 # Bugfixes
776 # Revision 1.6  2001/07/30 00:57:51  richard
777 # Now uses getopt, much improved command-line parsing. Much fuller help. Much
778 # better internal structure. It's just BETTER. :)
780 # Revision 1.5  2001/07/30 00:04:48  richard
781 # Made the "init" prompting more friendly.
783 # Revision 1.4  2001/07/29 07:01:39  richard
784 # Added vim command to all source so that we don't get no steenkin' tabs :)
786 # Revision 1.3  2001/07/23 08:45:28  richard
787 # ok, so now "./roundup-admin init" will ask questions in an attempt to get a
788 # workable instance_home set up :)
789 # _and_ anydbm has had its first test :)
791 # Revision 1.2  2001/07/23 08:20:44  richard
792 # Moved over to using marshal in the bsddb and anydbm backends.
793 # roundup-admin now has a "freshen" command that'll load/save all nodes (not
794 #  retired - mod hyperdb.Class.list() so it lists retired nodes)
796 # Revision 1.1  2001/07/23 03:46:48  richard
797 # moving the bin files to facilitate out-of-the-boxness
799 # Revision 1.1  2001/07/22 11:15:45  richard
800 # More Grande Splite stuff
803 # vim: set filetype=python ts=4 sw=4 et si