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