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 #
20 """Administration commands for maintaining Roundup trackers.
21 """
22 __docformat__ = 'restructuredtext'
24 import csv, getopt, getpass, os, re, shutil, sys, UserDict
26 from roundup import date, hyperdb, roundupdb, init, password, token
27 from roundup import __version__ as roundup_version
28 import roundup.instance
29 from roundup.configuration import CoreConfig
30 from roundup.i18n import _
31 from roundup.exceptions import UsageError
33 class CommandDict(UserDict.UserDict):
34 """Simple dictionary that lets us do lookups using partial keys.
36 Original code submitted by Engelbert Gruber.
37 """
38 _marker = []
39 def get(self, key, default=_marker):
40 if self.data.has_key(key):
41 return [(key, self.data[key])]
42 keylist = self.data.keys()
43 keylist.sort()
44 l = []
45 for ki in keylist:
46 if ki.startswith(key):
47 l.append((ki, self.data[ki]))
48 if not l and default is self._marker:
49 raise KeyError, key
50 return l
52 class AdminTool:
53 """ A collection of methods used in maintaining Roundup trackers.
55 Typically these methods are accessed through the roundup-admin
56 script. The main() method provided on this class gives the main
57 loop for the roundup-admin script.
59 Actions are defined by do_*() methods, with help for the action
60 given in the method docstring.
62 Additional help may be supplied by help_*() methods.
63 """
64 def __init__(self):
65 self.commands = CommandDict()
66 for k in AdminTool.__dict__.keys():
67 if k[:3] == 'do_':
68 self.commands[k[3:]] = getattr(self, k)
69 self.help = {}
70 for k in AdminTool.__dict__.keys():
71 if k[:5] == 'help_':
72 self.help[k[5:]] = getattr(self, k)
73 self.tracker_home = ''
74 self.db = None
75 self.db_uncommitted = False
77 def get_class(self, classname):
78 """Get the class - raise an exception if it doesn't exist.
79 """
80 try:
81 return self.db.getclass(classname)
82 except KeyError:
83 raise UsageError, _('no such class "%(classname)s"')%locals()
85 def props_from_args(self, args):
86 """ Produce a dictionary of prop: value from the args list.
88 The args list is specified as ``prop=value prop=value ...``.
89 """
90 props = {}
91 for arg in args:
92 if arg.find('=') == -1:
93 raise UsageError, _('argument "%(arg)s" not propname=value'
94 )%locals()
95 l = arg.split('=')
96 if len(l) < 2:
97 raise UsageError, _('argument "%(arg)s" not propname=value'
98 )%locals()
99 key, value = l[0], '='.join(l[1:])
100 if value:
101 props[key] = value
102 else:
103 props[key] = None
104 return props
106 def usage(self, message=''):
107 """ Display a simple usage message.
108 """
109 if message:
110 message = _('Problem: %(message)s\n\n')%locals()
111 print _("""%(message)sUsage: roundup-admin [options] [<command> <arguments>]
113 Options:
114 -i instance home -- specify the issue tracker "home directory" to administer
115 -u -- the user[:password] to use for commands
116 -d -- print full designators not just class id numbers
117 -c -- when outputting lists of data, comma-separate them.
118 Same as '-S ","'.
119 -S <string> -- when outputting lists of data, string-separate them
120 -s -- when outputting lists of data, space-separate them.
121 Same as '-S " "'.
122 -V -- be verbose when importing
123 -v -- report Roundup and Python versions (and quit)
125 Only one of -s, -c or -S can be specified.
127 Help:
128 roundup-admin -h
129 roundup-admin help -- this help
130 roundup-admin help <command> -- command-specific help
131 roundup-admin help all -- all available help
132 """)%locals()
133 self.help_commands()
135 def help_commands(self):
136 """List the commands available with their help summary.
137 """
138 print _('Commands:'),
139 commands = ['']
140 for command in self.commands.values():
141 h = _(command.__doc__).split('\n')[0]
142 commands.append(' '+h[7:])
143 commands.sort()
144 commands.append(_(
145 """Commands may be abbreviated as long as the abbreviation
146 matches only one command, e.g. l == li == lis == list."""))
147 print '\n'.join(commands)
148 print
150 def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')):
151 """ Produce an HTML command list.
152 """
153 commands = self.commands.values()
154 def sortfun(a, b):
155 return cmp(a.__name__, b.__name__)
156 commands.sort(sortfun)
157 for command in commands:
158 h = _(command.__doc__).split('\n')
159 name = command.__name__[3:]
160 usage = h[0]
161 print """
162 <tr><td valign=top><strong>%(name)s</strong></td>
163 <td><tt>%(usage)s</tt><p>
164 <pre>""" % locals()
165 indent = indent_re.match(h[3])
166 if indent: indent = len(indent.group(1))
167 for line in h[3:]:
168 if indent:
169 print line[indent:]
170 else:
171 print line
172 print '</pre></td></tr>\n'
174 def help_all(self):
175 print _("""
176 All commands (except help) require a tracker specifier. This is just
177 the path to the roundup tracker you're working with. A roundup tracker
178 is where roundup keeps the database and configuration file that defines
179 an issue tracker. It may be thought of as the issue tracker's "home
180 directory". It may be specified in the environment variable TRACKER_HOME
181 or on the command line as "-i tracker".
183 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
185 Property values are represented as strings in command arguments and in the
186 printed results:
187 . Strings are, well, strings.
188 . Date values are printed in the full date format in the local time zone,
189 and accepted in the full format or any of the partial formats explained
190 below.
191 . Link values are printed as node designators. When given as an argument,
192 node designators and key strings are both accepted.
193 . Multilink values are printed as lists of node designators joined
194 by commas. When given as an argument, node designators and key
195 strings are both accepted; an empty string, a single node, or a list
196 of nodes joined by commas is accepted.
198 When property values must contain spaces, just surround the value with
199 quotes, either ' or ". A single space may also be backslash-quoted. If a
200 value must contain a quote character, it must be backslash-quoted or inside
201 quotes. Examples:
202 hello world (2 tokens: hello, world)
203 "hello world" (1 token: hello world)
204 "Roch'e" Compaan (2 tokens: Roch'e Compaan)
205 Roch\\'e Compaan (2 tokens: Roch'e Compaan)
206 address="1 2 3" (1 token: address=1 2 3)
207 \\\\ (1 token: \\)
208 \\n\\r\\t (1 token: a newline, carriage-return and tab)
210 When multiple nodes are specified to the roundup get or roundup set
211 commands, the specified properties are retrieved or set on all the listed
212 nodes.
214 When multiple results are returned by the roundup get or roundup find
215 commands, they are printed one per line (default) or joined by commas (with
216 the -c) option.
218 Where the command changes data, a login name/password is required. The
219 login may be specified as either "name" or "name:password".
220 . ROUNDUP_LOGIN environment variable
221 . the -u command-line option
222 If either the name or password is not supplied, they are obtained from the
223 command-line.
225 Date format examples:
226 "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
227 "2000-04-17" means <Date 2000-04-17.00:00:00>
228 "01-25" means <Date yyyy-01-25.00:00:00>
229 "08-13.22:13" means <Date yyyy-08-14.03:13:00>
230 "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
231 "14:25" means <Date yyyy-mm-dd.19:25:00>
232 "8:47:11" means <Date yyyy-mm-dd.13:47:11>
233 "." means "right now"
235 Command help:
236 """)
237 for name, command in self.commands.items():
238 print _('%s:')%name
239 print ' ', _(command.__doc__)
241 def do_help(self, args, nl_re=re.compile('[\r\n]'),
242 indent_re=re.compile(r'^(\s+)\S+')):
243 ''"""Usage: help topic
244 Give help about topic.
246 commands -- list commands
247 <command> -- help specific to a command
248 initopts -- init command options
249 all -- all available help
250 """
251 if len(args)>0:
252 topic = args[0]
253 else:
254 topic = 'help'
257 # try help_ methods
258 if self.help.has_key(topic):
259 self.help[topic]()
260 return 0
262 # try command docstrings
263 try:
264 l = self.commands.get(topic)
265 except KeyError:
266 print _('Sorry, no help for "%(topic)s"')%locals()
267 return 1
269 # display the help for each match, removing the docsring indent
270 for name, help in l:
271 lines = nl_re.split(_(help.__doc__))
272 print lines[0]
273 indent = indent_re.match(lines[1])
274 if indent: indent = len(indent.group(1))
275 for line in lines[1:]:
276 if indent:
277 print line[indent:]
278 else:
279 print line
280 return 0
282 def listTemplates(self):
283 """ List all the available templates.
285 Look in the following places, where the later rules take precedence:
287 1. <roundup.admin.__file__>/../../share/roundup/templates/*
288 this is where they will be if we installed an egg via easy_install
289 2. <prefix>/share/roundup/templates/*
290 this should be the standard place to find them when Roundup is
291 installed
292 3. <roundup.admin.__file__>/../templates/*
293 this will be used if Roundup's run in the distro (aka. source)
294 directory
295 4. <current working dir>/*
296 this is for when someone unpacks a 3rd-party template
297 5. <current working dir>
298 this is for someone who "cd"s to the 3rd-party template dir
299 """
300 # OK, try <prefix>/share/roundup/templates
301 # and <egg-directory>/share/roundup/templates
302 # -- this module (roundup.admin) will be installed in something
303 # like:
304 # /usr/lib/python2.5/site-packages/roundup/admin.py (5 dirs up)
305 # c:\python25\lib\site-packages\roundup\admin.py (4 dirs up)
306 # /usr/lib/python2.5/site-packages/roundup-1.3.3-py2.5-egg/roundup/admin.py
307 # (2 dirs up)
308 #
309 # we're interested in where the directory containing "share" is
310 templates = {}
311 for N in 2, 4, 5:
312 path = __file__
313 # move up N elements in the path
314 for i in range(N):
315 path = os.path.dirname(path)
316 tdir = os.path.join(path, 'share', 'roundup', 'templates')
317 if os.path.isdir(tdir):
318 templates = init.listTemplates(tdir)
319 break
321 # OK, now try as if we're in the roundup source distribution
322 # directory, so this module will be in .../roundup-*/roundup/admin.py
323 # and we're interested in the .../roundup-*/ part.
324 path = __file__
325 for i in range(2):
326 path = os.path.dirname(path)
327 tdir = os.path.join(path, 'templates')
328 if os.path.isdir(tdir):
329 templates.update(init.listTemplates(tdir))
331 # Try subdirs of the current dir
332 templates.update(init.listTemplates(os.getcwd()))
334 # Finally, try the current directory as a template
335 template = init.loadTemplateInfo(os.getcwd())
336 if template:
337 templates[template['name']] = template
339 return templates
341 def help_initopts(self):
342 templates = self.listTemplates()
343 print _('Templates:'), ', '.join(templates.keys())
344 import roundup.backends
345 backends = roundup.backends.list_backends()
346 print _('Back ends:'), ', '.join(backends)
348 def do_install(self, tracker_home, args):
349 ''"""Usage: install [template [backend [key=val[,key=val]]]]
350 Install a new Roundup tracker.
352 The command will prompt for the tracker home directory
353 (if not supplied through TRACKER_HOME or the -i option).
354 The template and backend may be specified on the command-line
355 as arguments, in that order.
357 Command line arguments following the backend allows you to
358 pass initial values for config options. For example, passing
359 "web_http_auth=no,rdbms_user=dinsdale" will override defaults
360 for options http_auth in section [web] and user in section [rdbms].
361 Please be careful to not use spaces in this argument! (Enclose
362 whole argument in quotes if you need spaces in option value).
364 The initialise command must be called after this command in order
365 to initialise the tracker's database. You may edit the tracker's
366 initial database contents before running that command by editing
367 the tracker's dbinit.py module init() function.
369 See also initopts help.
370 """
371 if len(args) < 1:
372 raise UsageError, _('Not enough arguments supplied')
374 # make sure the tracker home can be created
375 tracker_home = os.path.abspath(tracker_home)
376 parent = os.path.split(tracker_home)[0]
377 if not os.path.exists(parent):
378 raise UsageError, _('Instance home parent directory "%(parent)s"'
379 ' does not exist')%locals()
381 config_ini_file = os.path.join(tracker_home, CoreConfig.INI_FILE)
382 # check for both old- and new-style configs
383 if filter(os.path.exists, [config_ini_file,
384 os.path.join(tracker_home, 'config.py')]):
385 ok = raw_input(_(
386 """WARNING: There appears to be a tracker in "%(tracker_home)s"!
387 If you re-install it, you will lose all the data!
388 Erase it? Y/N: """) % locals())
389 if ok.strip().lower() != 'y':
390 return 0
392 # clear it out so the install isn't confused
393 shutil.rmtree(tracker_home)
395 # select template
396 templates = self.listTemplates()
397 template = len(args) > 1 and args[1] or ''
398 if not templates.has_key(template):
399 print _('Templates:'), ', '.join(templates.keys())
400 while not templates.has_key(template):
401 template = raw_input(_('Select template [classic]: ')).strip()
402 if not template:
403 template = 'classic'
405 # select hyperdb backend
406 import roundup.backends
407 backends = roundup.backends.list_backends()
408 backend = len(args) > 2 and args[2] or ''
409 if backend not in backends:
410 print _('Back ends:'), ', '.join(backends)
411 while backend not in backends:
412 backend = raw_input(_('Select backend [anydbm]: ')).strip()
413 if not backend:
414 backend = 'anydbm'
415 # XXX perform a unit test based on the user's selections
417 # Process configuration file definitions
418 if len(args) > 3:
419 try:
420 defns = dict([item.split("=") for item in args[3].split(",")])
421 except:
422 print _('Error in configuration settings: "%s"') % args[3]
423 raise
424 else:
425 defns = {}
427 # install!
428 init.install(tracker_home, templates[template]['path'], settings=defns)
429 init.write_select_db(tracker_home, backend)
431 print _("""
432 ---------------------------------------------------------------------------
433 You should now edit the tracker configuration file:
434 %(config_file)s""") % {"config_file": config_ini_file}
436 # find list of options that need manual adjustments
437 # XXX config._get_unset_options() is marked as private
438 # (leading underscore). make it public or don't care?
439 need_set = CoreConfig(tracker_home)._get_unset_options()
440 if need_set:
441 print _(" ... at a minimum, you must set following options:")
442 for section, options in need_set.items():
443 print " [%s]: %s" % (section, ", ".join(options))
445 # note about schema modifications
446 print _("""
447 If you wish to modify the database schema,
448 you should also edit the schema file:
449 %(database_config_file)s
450 You may also change the database initialisation file:
451 %(database_init_file)s
452 ... see the documentation on customizing for more information.
454 You MUST run the "roundup-admin initialise" command once you've performed
455 the above steps.
456 ---------------------------------------------------------------------------
457 """) % {
458 'database_config_file': os.path.join(tracker_home, 'schema.py'),
459 'database_init_file': os.path.join(tracker_home, 'initial_data.py'),
460 }
461 return 0
463 def do_genconfig(self, args):
464 ''"""Usage: genconfig <filename>
465 Generate a new tracker config file (ini style) with default values
466 in <filename>.
467 """
468 if len(args) < 1:
469 raise UsageError, _('Not enough arguments supplied')
470 config = CoreConfig()
471 config.save(args[0])
473 def do_initialise(self, tracker_home, args):
474 ''"""Usage: initialise [adminpw]
475 Initialise a new Roundup tracker.
477 The administrator details will be set at this step.
479 Execute the tracker's initialisation function dbinit.init()
480 """
481 # password
482 if len(args) > 1:
483 adminpw = args[1]
484 else:
485 adminpw = ''
486 confirm = 'x'
487 while adminpw != confirm:
488 adminpw = getpass.getpass(_('Admin Password: '))
489 confirm = getpass.getpass(_(' Confirm: '))
491 # make sure the tracker home is installed
492 if not os.path.exists(tracker_home):
493 raise UsageError, _('Instance home does not exist')%locals()
494 try:
495 tracker = roundup.instance.open(tracker_home)
496 except roundup.instance.TrackerError:
497 raise UsageError, _('Instance has not been installed')%locals()
499 # is there already a database?
500 if tracker.exists():
501 ok = raw_input(_(
502 """WARNING: The database is already initialised!
503 If you re-initialise it, you will lose all the data!
504 Erase it? Y/N: """))
505 if ok.strip().lower() != 'y':
506 return 0
508 backend = tracker.get_backend_name()
510 # nuke it
511 tracker.nuke()
513 # re-write the backend select file
514 init.write_select_db(tracker_home, backend)
516 # GO
517 tracker.init(password.Password(adminpw))
519 return 0
522 def do_get(self, args):
523 ''"""Usage: get property designator[,designator]*
524 Get the given property of one or more designator(s).
526 A designator is a classname and a nodeid concatenated,
527 eg. bug1, user10, ...
529 Retrieves the property value of the nodes specified
530 by the designators.
531 """
532 if len(args) < 2:
533 raise UsageError, _('Not enough arguments supplied')
534 propname = args[0]
535 designators = args[1].split(',')
536 l = []
537 for designator in designators:
538 # decode the node designator
539 try:
540 classname, nodeid = hyperdb.splitDesignator(designator)
541 except hyperdb.DesignatorError, message:
542 raise UsageError, message
544 # get the class
545 cl = self.get_class(classname)
546 try:
547 id=[]
548 if self.separator:
549 if self.print_designator:
550 # see if property is a link or multilink for
551 # which getting a desginator make sense.
552 # Algorithm: Get the properties of the
553 # current designator's class. (cl.getprops)
554 # get the property object for the property the
555 # user requested (properties[propname])
556 # verify its type (isinstance...)
557 # raise error if not link/multilink
558 # get class name for link/multilink property
559 # do the get on the designators
560 # append the new designators
561 # print
562 properties = cl.getprops()
563 property = properties[propname]
564 if not (isinstance(property, hyperdb.Multilink) or
565 isinstance(property, hyperdb.Link)):
566 raise UsageError, _('property %s is not of type Multilink or Link so -d flag does not apply.')%propname
567 propclassname = self.db.getclass(property.classname).classname
568 id = cl.get(nodeid, propname)
569 for i in id:
570 l.append(propclassname + i)
571 else:
572 id = cl.get(nodeid, propname)
573 for i in id:
574 l.append(i)
575 else:
576 if self.print_designator:
577 properties = cl.getprops()
578 property = properties[propname]
579 if not (isinstance(property, hyperdb.Multilink) or
580 isinstance(property, hyperdb.Link)):
581 raise UsageError, _('property %s is not of type Multilink or Link so -d flag does not apply.')%propname
582 propclassname = self.db.getclass(property.classname).classname
583 id = cl.get(nodeid, propname)
584 for i in id:
585 print propclassname + i
586 else:
587 print cl.get(nodeid, propname)
588 except IndexError:
589 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
590 except KeyError:
591 raise UsageError, _('no such %(classname)s property '
592 '"%(propname)s"')%locals()
593 if self.separator:
594 print self.separator.join(l)
596 return 0
599 def do_set(self, args):
600 ''"""Usage: set items property=value property=value ...
601 Set the given properties of one or more items(s).
603 The items are specified as a class or as a comma-separated
604 list of item designators (ie "designator[,designator,...]").
606 A designator is a classname and a nodeid concatenated,
607 eg. bug1, user10, ...
609 This command sets the properties to the values for all designators
610 given. If the value is missing (ie. "property=") then the property
611 is un-set. If the property is a multilink, you specify the linked
612 ids for the multilink as comma-separated numbers (ie "1,2,3").
613 """
614 if len(args) < 2:
615 raise UsageError, _('Not enough arguments supplied')
616 from roundup import hyperdb
618 designators = args[0].split(',')
619 if len(designators) == 1:
620 designator = designators[0]
621 try:
622 designator = hyperdb.splitDesignator(designator)
623 designators = [designator]
624 except hyperdb.DesignatorError:
625 cl = self.get_class(designator)
626 designators = [(designator, x) for x in cl.list()]
627 else:
628 try:
629 designators = [hyperdb.splitDesignator(x) for x in designators]
630 except hyperdb.DesignatorError, message:
631 raise UsageError, message
633 # get the props from the args
634 props = self.props_from_args(args[1:])
636 # now do the set for all the nodes
637 for classname, itemid in designators:
638 cl = self.get_class(classname)
640 properties = cl.getprops()
641 for key, value in props.items():
642 try:
643 props[key] = hyperdb.rawToHyperdb(self.db, cl, itemid,
644 key, value)
645 except hyperdb.HyperdbValueError, message:
646 raise UsageError, message
648 # try the set
649 try:
650 apply(cl.set, (itemid, ), props)
651 except (TypeError, IndexError, ValueError), message:
652 import traceback; traceback.print_exc()
653 raise UsageError, message
654 self.db_uncommitted = True
655 return 0
657 def do_find(self, args):
658 ''"""Usage: find classname propname=value ...
659 Find the nodes of the given class with a given link property value.
661 Find the nodes of the given class with a given link property value.
662 The value may be either the nodeid of the linked node, or its key
663 value.
664 """
665 if len(args) < 1:
666 raise UsageError, _('Not enough arguments supplied')
667 classname = args[0]
668 # get the class
669 cl = self.get_class(classname)
671 # handle the propname=value argument
672 props = self.props_from_args(args[1:])
674 # convert the user-input value to a value used for find()
675 for propname, value in props.items():
676 if ',' in value:
677 values = value.split(',')
678 else:
679 values = [value]
680 d = props[propname] = {}
681 for value in values:
682 value = hyperdb.rawToHyperdb(self.db, cl, None, propname, value)
683 if isinstance(value, list):
684 for entry in value:
685 d[entry] = 1
686 else:
687 d[value] = 1
689 # now do the find
690 try:
691 id = []
692 designator = []
693 if self.separator:
694 if self.print_designator:
695 id=apply(cl.find, (), props)
696 for i in id:
697 designator.append(classname + i)
698 print self.separator.join(designator)
699 else:
700 print self.separator.join(apply(cl.find, (), props))
702 else:
703 if self.print_designator:
704 id=apply(cl.find, (), props)
705 for i in id:
706 designator.append(classname + i)
707 print designator
708 else:
709 print apply(cl.find, (), props)
710 except KeyError:
711 raise UsageError, _('%(classname)s has no property '
712 '"%(propname)s"')%locals()
713 except (ValueError, TypeError), message:
714 raise UsageError, message
715 return 0
717 def do_specification(self, args):
718 ''"""Usage: specification classname
719 Show the properties for a classname.
721 This lists the properties for a given class.
722 """
723 if len(args) < 1:
724 raise UsageError, _('Not enough arguments supplied')
725 classname = args[0]
726 # get the class
727 cl = self.get_class(classname)
729 # get the key property
730 keyprop = cl.getkey()
731 for key, value in cl.properties.items():
732 if keyprop == key:
733 print _('%(key)s: %(value)s (key property)')%locals()
734 else:
735 print _('%(key)s: %(value)s')%locals()
737 def do_display(self, args):
738 ''"""Usage: display designator[,designator]*
739 Show the property values for the given node(s).
741 A designator is a classname and a nodeid concatenated,
742 eg. bug1, user10, ...
744 This lists the properties and their associated values for the given
745 node.
746 """
747 if len(args) < 1:
748 raise UsageError, _('Not enough arguments supplied')
750 # decode the node designator
751 for designator in args[0].split(','):
752 try:
753 classname, nodeid = hyperdb.splitDesignator(designator)
754 except hyperdb.DesignatorError, message:
755 raise UsageError, message
757 # get the class
758 cl = self.get_class(classname)
760 # display the values
761 keys = cl.properties.keys()
762 keys.sort()
763 for key in keys:
764 value = cl.get(nodeid, key)
765 print _('%(key)s: %(value)s')%locals()
767 def do_create(self, args):
768 ''"""Usage: create classname property=value ...
769 Create a new entry of a given class.
771 This creates a new entry of the given class using the property
772 name=value arguments provided on the command line after the "create"
773 command.
774 """
775 if len(args) < 1:
776 raise UsageError, _('Not enough arguments supplied')
777 from roundup import hyperdb
779 classname = args[0]
781 # get the class
782 cl = self.get_class(classname)
784 # now do a create
785 props = {}
786 properties = cl.getprops(protected = 0)
787 if len(args) == 1:
788 # ask for the properties
789 for key, value in properties.items():
790 if key == 'id': continue
791 name = value.__class__.__name__
792 if isinstance(value , hyperdb.Password):
793 again = None
794 while value != again:
795 value = getpass.getpass(_('%(propname)s (Password): ')%{
796 'propname': key.capitalize()})
797 again = getpass.getpass(_(' %(propname)s (Again): ')%{
798 'propname': key.capitalize()})
799 if value != again: print _('Sorry, try again...')
800 if value:
801 props[key] = value
802 else:
803 value = raw_input(_('%(propname)s (%(proptype)s): ')%{
804 'propname': key.capitalize(), 'proptype': name})
805 if value:
806 props[key] = value
807 else:
808 props = self.props_from_args(args[1:])
810 # convert types
811 for propname, value in props.items():
812 try:
813 props[propname] = hyperdb.rawToHyperdb(self.db, cl, None,
814 propname, value)
815 except hyperdb.HyperdbValueError, message:
816 raise UsageError, message
818 # check for the key property
819 propname = cl.getkey()
820 if propname and not props.has_key(propname):
821 raise UsageError, _('you must provide the "%(propname)s" '
822 'property.')%locals()
824 # do the actual create
825 try:
826 print apply(cl.create, (), props)
827 except (TypeError, IndexError, ValueError), message:
828 raise UsageError, message
829 self.db_uncommitted = True
830 return 0
832 def do_list(self, args):
833 ''"""Usage: list classname [property]
834 List the instances of a class.
836 Lists all instances of the given class. If the property is not
837 specified, the "label" property is used. The label property is
838 tried in order: the key, "name", "title" and then the first
839 property, alphabetically.
841 With -c, -S or -s print a list of item id's if no property
842 specified. If property specified, print list of that property
843 for every class instance.
844 """
845 if len(args) > 2:
846 raise UsageError, _('Too many arguments supplied')
847 if len(args) < 1:
848 raise UsageError, _('Not enough arguments supplied')
849 classname = args[0]
851 # get the class
852 cl = self.get_class(classname)
854 # figure the property
855 if len(args) > 1:
856 propname = args[1]
857 else:
858 propname = cl.labelprop()
860 if self.separator:
861 if len(args) == 2:
862 # create a list of propnames since user specified propname
863 proplist=[]
864 for nodeid in cl.list():
865 try:
866 proplist.append(cl.get(nodeid, propname))
867 except KeyError:
868 raise UsageError, _('%(classname)s has no property '
869 '"%(propname)s"')%locals()
870 print self.separator.join(proplist)
871 else:
872 # create a list of index id's since user didn't specify
873 # otherwise
874 print self.separator.join(cl.list())
875 else:
876 for nodeid in cl.list():
877 try:
878 value = cl.get(nodeid, propname)
879 except KeyError:
880 raise UsageError, _('%(classname)s has no property '
881 '"%(propname)s"')%locals()
882 print _('%(nodeid)4s: %(value)s')%locals()
883 return 0
885 def do_table(self, args):
886 ''"""Usage: table classname [property[,property]*]
887 List the instances of a class in tabular form.
889 Lists all instances of the given class. If the properties are not
890 specified, all properties are displayed. By default, the column
891 widths are the width of the largest value. The width may be
892 explicitly defined by defining the property as "name:width".
893 For example::
895 roundup> table priority id,name:10
896 Id Name
897 1 fatal-bug
898 2 bug
899 3 usability
900 4 feature
902 Also to make the width of the column the width of the label,
903 leave a trailing : without a width on the property. For example::
905 roundup> table priority id,name:
906 Id Name
907 1 fata
908 2 bug
909 3 usab
910 4 feat
912 will result in a the 4 character wide "Name" column.
913 """
914 if len(args) < 1:
915 raise UsageError, _('Not enough arguments supplied')
916 classname = args[0]
918 # get the class
919 cl = self.get_class(classname)
921 # figure the property names to display
922 if len(args) > 1:
923 prop_names = args[1].split(',')
924 all_props = cl.getprops()
925 for spec in prop_names:
926 if ':' in spec:
927 try:
928 propname, width = spec.split(':')
929 except (ValueError, TypeError):
930 raise UsageError, _('"%(spec)s" not name:width')%locals()
931 else:
932 propname = spec
933 if not all_props.has_key(propname):
934 raise UsageError, _('%(classname)s has no property '
935 '"%(propname)s"')%locals()
936 else:
937 prop_names = cl.getprops().keys()
939 # now figure column widths
940 props = []
941 for spec in prop_names:
942 if ':' in spec:
943 name, width = spec.split(':')
944 if width == '':
945 props.append((name, len(spec)))
946 else:
947 props.append((name, int(width)))
948 else:
949 # this is going to be slow
950 maxlen = len(spec)
951 for nodeid in cl.list():
952 curlen = len(str(cl.get(nodeid, spec)))
953 if curlen > maxlen:
954 maxlen = curlen
955 props.append((spec, maxlen))
957 # now display the heading
958 print ' '.join([name.capitalize().ljust(width) for name,width in props])
960 # and the table data
961 for nodeid in cl.list():
962 l = []
963 for name, width in props:
964 if name != 'id':
965 try:
966 value = str(cl.get(nodeid, name))
967 except KeyError:
968 # we already checked if the property is valid - a
969 # KeyError here means the node just doesn't have a
970 # value for it
971 value = ''
972 else:
973 value = str(nodeid)
974 f = '%%-%ds'%width
975 l.append(f%value[:width])
976 print ' '.join(l)
977 return 0
979 def do_history(self, args):
980 ''"""Usage: history designator
981 Show the history entries of a designator.
983 A designator is a classname and a nodeid concatenated,
984 eg. bug1, user10, ...
986 Lists the journal entries for the node identified by the designator.
987 """
988 if len(args) < 1:
989 raise UsageError, _('Not enough arguments supplied')
990 try:
991 classname, nodeid = hyperdb.splitDesignator(args[0])
992 except hyperdb.DesignatorError, message:
993 raise UsageError, message
995 try:
996 print self.db.getclass(classname).history(nodeid)
997 except KeyError:
998 raise UsageError, _('no such class "%(classname)s"')%locals()
999 except IndexError:
1000 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
1001 return 0
1003 def do_commit(self, args):
1004 ''"""Usage: commit
1005 Commit changes made to the database during an interactive session.
1007 The changes made during an interactive session are not
1008 automatically written to the database - they must be committed
1009 using this command.
1011 One-off commands on the command-line are automatically committed if
1012 they are successful.
1013 """
1014 self.db.commit()
1015 self.db_uncommitted = False
1016 return 0
1018 def do_rollback(self, args):
1019 ''"""Usage: rollback
1020 Undo all changes that are pending commit to the database.
1022 The changes made during an interactive session are not
1023 automatically written to the database - they must be committed
1024 manually. This command undoes all those changes, so a commit
1025 immediately after would make no changes to the database.
1026 """
1027 self.db.rollback()
1028 self.db_uncommitted = False
1029 return 0
1031 def do_retire(self, args):
1032 ''"""Usage: retire designator[,designator]*
1033 Retire the node specified by designator.
1035 A designator is a classname and a nodeid concatenated,
1036 eg. bug1, user10, ...
1038 This action indicates that a particular node is not to be retrieved
1039 by the list or find commands, and its key value may be re-used.
1040 """
1041 if len(args) < 1:
1042 raise UsageError, _('Not enough arguments supplied')
1043 designators = args[0].split(',')
1044 for designator in designators:
1045 try:
1046 classname, nodeid = hyperdb.splitDesignator(designator)
1047 except hyperdb.DesignatorError, message:
1048 raise UsageError, message
1049 try:
1050 self.db.getclass(classname).retire(nodeid)
1051 except KeyError:
1052 raise UsageError, _('no such class "%(classname)s"')%locals()
1053 except IndexError:
1054 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
1055 self.db_uncommitted = True
1056 return 0
1058 def do_restore(self, args):
1059 ''"""Usage: restore designator[,designator]*
1060 Restore the retired node specified by designator.
1062 A designator is a classname and a nodeid concatenated,
1063 eg. bug1, user10, ...
1065 The given nodes will become available for users again.
1066 """
1067 if len(args) < 1:
1068 raise UsageError, _('Not enough arguments supplied')
1069 designators = args[0].split(',')
1070 for designator in designators:
1071 try:
1072 classname, nodeid = hyperdb.splitDesignator(designator)
1073 except hyperdb.DesignatorError, message:
1074 raise UsageError, message
1075 try:
1076 self.db.getclass(classname).restore(nodeid)
1077 except KeyError:
1078 raise UsageError, _('no such class "%(classname)s"')%locals()
1079 except IndexError:
1080 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
1081 self.db_uncommitted = True
1082 return 0
1084 def do_export(self, args, export_files=True):
1085 ''"""Usage: export [[-]class[,class]] export_dir
1086 Export the database to colon-separated-value files.
1087 To exclude the files (e.g. for the msg or file class),
1088 use the exporttables command.
1090 Optionally limit the export to just the named classes
1091 or exclude the named classes, if the 1st argument starts with '-'.
1093 This action exports the current data from the database into
1094 colon-separated-value files that are placed in the nominated
1095 destination directory.
1096 """
1097 # grab the directory to export to
1098 if len(args) < 1:
1099 raise UsageError, _('Not enough arguments supplied')
1101 dir = args[-1]
1103 # get the list of classes to export
1104 if len(args) == 2:
1105 if args[0].startswith('-'):
1106 classes = [ c for c in self.db.classes.keys()
1107 if not c in args[0][1:].split(',') ]
1108 else:
1109 classes = args[0].split(',')
1110 else:
1111 classes = self.db.classes.keys()
1113 class colon_separated(csv.excel):
1114 delimiter = ':'
1116 # make sure target dir exists
1117 if not os.path.exists(dir):
1118 os.makedirs(dir)
1120 # maximum csv field length exceeding configured size?
1121 max_len = self.db.config.CSV_FIELD_SIZE
1123 # do all the classes specified
1124 for classname in classes:
1125 cl = self.get_class(classname)
1127 if not export_files and hasattr(cl, 'export_files'):
1128 sys.stdout.write('Exporting %s WITHOUT the files\r\n'%
1129 classname)
1131 f = open(os.path.join(dir, classname+'.csv'), 'wb')
1132 writer = csv.writer(f, colon_separated)
1134 properties = cl.getprops()
1135 propnames = cl.export_propnames()
1136 fields = propnames[:]
1137 fields.append('is retired')
1138 writer.writerow(fields)
1140 # all nodes for this class
1141 for nodeid in cl.getnodeids():
1142 if self.verbose:
1143 sys.stdout.write('\rExporting %s - %s'%(classname, nodeid))
1144 sys.stdout.flush()
1145 node = cl.getnode(nodeid)
1146 exp = cl.export_list(propnames, nodeid)
1147 lensum = sum ([len (repr(node[p])) for p in propnames])
1148 # for a safe upper bound of field length we add
1149 # difference between CSV len and sum of all field lengths
1150 d = sum ([len(x) for x in exp]) - lensum
1151 assert (d > 0)
1152 for p in propnames:
1153 ll = len(repr(node[p])) + d
1154 if ll > max_len:
1155 max_len = ll
1156 writer.writerow(exp)
1157 if export_files and hasattr(cl, 'export_files'):
1158 cl.export_files(dir, nodeid)
1160 # close this file
1161 f.close()
1163 # export the journals
1164 jf = open(os.path.join(dir, classname+'-journals.csv'), 'wb')
1165 if self.verbose:
1166 sys.stdout.write("\nExporting Journal for %s\n" % classname)
1167 sys.stdout.flush()
1168 journals = csv.writer(jf, colon_separated)
1169 map(journals.writerow, cl.export_journals())
1170 jf.close()
1171 if max_len > self.db.config.CSV_FIELD_SIZE:
1172 print >> sys.stderr, \
1173 "Warning: config csv_field_size should be at least %s"%max_len
1174 return 0
1176 def do_exporttables(self, args):
1177 ''"""Usage: exporttables [[-]class[,class]] export_dir
1178 Export the database to colon-separated-value files, excluding the
1179 files below $TRACKER_HOME/db/files/ (which can be archived separately).
1180 To include the files, use the export command.
1182 Optionally limit the export to just the named classes
1183 or exclude the named classes, if the 1st argument starts with '-'.
1185 This action exports the current data from the database into
1186 colon-separated-value files that are placed in the nominated
1187 destination directory.
1188 """
1189 return self.do_export(args, export_files=False)
1191 def do_import(self, args):
1192 ''"""Usage: import import_dir
1193 Import a database from the directory containing CSV files,
1194 two per class to import.
1196 The files used in the import are:
1198 <class>.csv
1199 This must define the same properties as the class (including
1200 having a "header" line with those property names.)
1201 <class>-journals.csv
1202 This defines the journals for the items being imported.
1204 The imported nodes will have the same nodeid as defined in the
1205 import file, thus replacing any existing content.
1207 The new nodes are added to the existing database - if you want to
1208 create a new database using the imported data, then create a new
1209 database (or, tediously, retire all the old data.)
1210 """
1211 if len(args) < 1:
1212 raise UsageError, _('Not enough arguments supplied')
1213 from roundup import hyperdb
1215 if hasattr (csv, 'field_size_limit'):
1216 csv.field_size_limit(self.db.config.CSV_FIELD_SIZE)
1218 # directory to import from
1219 dir = args[0]
1221 class colon_separated(csv.excel):
1222 delimiter = ':'
1224 # import all the files
1225 for file in os.listdir(dir):
1226 classname, ext = os.path.splitext(file)
1227 # we only care about CSV files
1228 if ext != '.csv' or classname.endswith('-journals'):
1229 continue
1231 cl = self.get_class(classname)
1233 # ensure that the properties and the CSV file headings match
1234 f = open(os.path.join(dir, file), 'r')
1235 reader = csv.reader(f, colon_separated)
1236 file_props = None
1237 maxid = 1
1238 # loop through the file and create a node for each entry
1239 for n, r in enumerate(reader):
1240 if file_props is None:
1241 file_props = r
1242 continue
1244 if self.verbose:
1245 sys.stdout.write('\rImporting %s - %s'%(classname, n))
1246 sys.stdout.flush()
1248 # do the import and figure the current highest nodeid
1249 nodeid = cl.import_list(file_props, r)
1250 if hasattr(cl, 'import_files'):
1251 cl.import_files(dir, nodeid)
1252 maxid = max(maxid, int(nodeid))
1253 print >> sys.stdout
1254 f.close()
1256 # import the journals
1257 f = open(os.path.join(args[0], classname + '-journals.csv'), 'r')
1258 reader = csv.reader(f, colon_separated)
1259 cl.import_journals(reader)
1260 f.close()
1262 # set the id counter
1263 print >> sys.stdout, 'setting', classname, maxid+1
1264 self.db.setid(classname, str(maxid+1))
1266 self.db_uncommitted = True
1267 return 0
1269 def do_pack(self, args):
1270 ''"""Usage: pack period | date
1272 Remove journal entries older than a period of time specified or
1273 before a certain date.
1275 A period is specified using the suffixes "y", "m", and "d". The
1276 suffix "w" (for "week") means 7 days.
1278 "3y" means three years
1279 "2y 1m" means two years and one month
1280 "1m 25d" means one month and 25 days
1281 "2w 3d" means two weeks and three days
1283 Date format is "YYYY-MM-DD" eg:
1284 2001-01-01
1286 """
1287 if len(args) <> 1:
1288 raise UsageError, _('Not enough arguments supplied')
1290 # are we dealing with a period or a date
1291 value = args[0]
1292 date_re = re.compile(r"""
1293 (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
1294 (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
1295 """, re.VERBOSE)
1296 m = date_re.match(value)
1297 if not m:
1298 raise ValueError, _('Invalid format')
1299 m = m.groupdict()
1300 if m['period']:
1301 pack_before = date.Date(". - %s"%value)
1302 elif m['date']:
1303 pack_before = date.Date(value)
1304 self.db.pack(pack_before)
1305 self.db_uncommitted = True
1306 return 0
1308 def do_reindex(self, args, desre=re.compile('([A-Za-z]+)([0-9]+)')):
1309 ''"""Usage: reindex [classname|designator]*
1310 Re-generate a tracker's search indexes.
1312 This will re-generate the search indexes for a tracker.
1313 This will typically happen automatically.
1314 """
1315 if args:
1316 for arg in args:
1317 m = desre.match(arg)
1318 if m:
1319 cl = self.get_class(m.group(1))
1320 try:
1321 cl.index(m.group(2))
1322 except IndexError:
1323 raise UsageError, _('no such item "%(designator)s"')%{
1324 'designator': arg}
1325 else:
1326 cl = self.get_class(arg)
1327 self.db.reindex(arg)
1328 else:
1329 self.db.reindex(show_progress=True)
1330 return 0
1332 def do_security(self, args):
1333 ''"""Usage: security [Role name]
1334 Display the Permissions available to one or all Roles.
1335 """
1336 if len(args) == 1:
1337 role = args[0]
1338 try:
1339 roles = [(args[0], self.db.security.role[args[0]])]
1340 except KeyError:
1341 print _('No such Role "%(role)s"')%locals()
1342 return 1
1343 else:
1344 roles = self.db.security.role.items()
1345 role = self.db.config.NEW_WEB_USER_ROLES
1346 if ',' in role:
1347 print _('New Web users get the Roles "%(role)s"')%locals()
1348 else:
1349 print _('New Web users get the Role "%(role)s"')%locals()
1350 role = self.db.config.NEW_EMAIL_USER_ROLES
1351 if ',' in role:
1352 print _('New Email users get the Roles "%(role)s"')%locals()
1353 else:
1354 print _('New Email users get the Role "%(role)s"')%locals()
1355 roles.sort()
1356 for rolename, role in roles:
1357 print _('Role "%(name)s":')%role.__dict__
1358 for permission in role.permissions:
1359 d = permission.__dict__
1360 if permission.klass:
1361 if permission.properties:
1362 print _(' %(description)s (%(name)s for "%(klass)s"'
1363 ': %(properties)s only)')%d
1364 else:
1365 print _(' %(description)s (%(name)s for "%(klass)s" '
1366 'only)')%d
1367 else:
1368 print _(' %(description)s (%(name)s)')%d
1369 return 0
1372 def do_migrate(self, args):
1373 ''"""Usage: migrate
1374 Update a tracker's database to be compatible with the Roundup
1375 codebase.
1377 You should run the "migrate" command for your tracker once you've
1378 installed the latest codebase.
1380 Do this before you use the web, command-line or mail interface and
1381 before any users access the tracker.
1383 This command will respond with either "Tracker updated" (if you've
1384 not previously run it on an RDBMS backend) or "No migration action
1385 required" (if you have run it, or have used another interface to the
1386 tracker, or possibly because you are using anydbm).
1388 It's safe to run this even if it's not required, so just get into
1389 the habit.
1390 """
1391 if getattr(self.db, 'db_version_updated'):
1392 print _('Tracker updated')
1393 self.db_uncommitted = True
1394 else:
1395 print _('No migration action required')
1396 return 0
1398 def run_command(self, args):
1399 """Run a single command
1400 """
1401 command = args[0]
1403 # handle help now
1404 if command == 'help':
1405 if len(args)>1:
1406 self.do_help(args[1:])
1407 return 0
1408 self.do_help(['help'])
1409 return 0
1410 if command == 'morehelp':
1411 self.do_help(['help'])
1412 self.help_commands()
1413 self.help_all()
1414 return 0
1415 if command == 'config':
1416 self.do_config(args[1:])
1417 return 0
1419 # figure what the command is
1420 try:
1421 functions = self.commands.get(command)
1422 except KeyError:
1423 # not a valid command
1424 print _('Unknown command "%(command)s" ("help commands" for a '
1425 'list)')%locals()
1426 return 1
1428 # check for multiple matches
1429 if len(functions) > 1:
1430 print _('Multiple commands match "%(command)s": %(list)s')%{'command':
1431 command, 'list': ', '.join([i[0] for i in functions])}
1432 return 1
1433 command, function = functions[0]
1435 # make sure we have a tracker_home
1436 while not self.tracker_home:
1437 self.tracker_home = raw_input(_('Enter tracker home: ')).strip()
1439 # before we open the db, we may be doing an install or init
1440 if command == 'initialise':
1441 try:
1442 return self.do_initialise(self.tracker_home, args)
1443 except UsageError, message:
1444 print _('Error: %(message)s')%locals()
1445 return 1
1446 elif command == 'install':
1447 try:
1448 return self.do_install(self.tracker_home, args)
1449 except UsageError, message:
1450 print _('Error: %(message)s')%locals()
1451 return 1
1453 # get the tracker
1454 try:
1455 tracker = roundup.instance.open(self.tracker_home)
1456 except ValueError, message:
1457 self.tracker_home = ''
1458 print _("Error: Couldn't open tracker: %(message)s")%locals()
1459 return 1
1461 # only open the database once!
1462 if not self.db:
1463 self.db = tracker.open('admin')
1465 # do the command
1466 ret = 0
1467 try:
1468 ret = function(args[1:])
1469 except UsageError, message:
1470 print _('Error: %(message)s')%locals()
1471 print
1472 print function.__doc__
1473 ret = 1
1474 except:
1475 import traceback
1476 traceback.print_exc()
1477 ret = 1
1478 return ret
1480 def interactive(self):
1481 """Run in an interactive mode
1482 """
1483 print _('Roundup %s ready for input.\nType "help" for help.'
1484 % roundup_version)
1485 try:
1486 import readline
1487 except ImportError:
1488 print _('Note: command history and editing not available')
1490 while 1:
1491 try:
1492 command = raw_input(_('roundup> '))
1493 except EOFError:
1494 print _('exit...')
1495 break
1496 if not command: continue
1497 args = token.token_split(command)
1498 if not args: continue
1499 if args[0] in ('quit', 'exit'): break
1500 self.run_command(args)
1502 # exit.. check for transactions
1503 if self.db and self.db_uncommitted:
1504 commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1505 if commit and commit[0].lower() == 'y':
1506 self.db.commit()
1507 return 0
1509 def main(self):
1510 try:
1511 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hcdsS:vV')
1512 except getopt.GetoptError, e:
1513 self.usage(str(e))
1514 return 1
1516 # handle command-line args
1517 self.tracker_home = os.environ.get('TRACKER_HOME', '')
1518 # TODO: reinstate the user/password stuff (-u arg too)
1519 name = password = ''
1520 if os.environ.has_key('ROUNDUP_LOGIN'):
1521 l = os.environ['ROUNDUP_LOGIN'].split(':')
1522 name = l[0]
1523 if len(l) > 1:
1524 password = l[1]
1525 self.separator = None
1526 self.print_designator = 0
1527 self.verbose = 0
1528 for opt, arg in opts:
1529 if opt == '-h':
1530 self.usage()
1531 return 0
1532 elif opt == '-v':
1533 print '%s (python %s)'%(roundup_version, sys.version.split()[0])
1534 return 0
1535 elif opt == '-V':
1536 self.verbose = 1
1537 elif opt == '-i':
1538 self.tracker_home = arg
1539 elif opt == '-c':
1540 if self.separator != None:
1541 self.usage('Only one of -c, -S and -s may be specified')
1542 return 1
1543 self.separator = ','
1544 elif opt == '-S':
1545 if self.separator != None:
1546 self.usage('Only one of -c, -S and -s may be specified')
1547 return 1
1548 self.separator = arg
1549 elif opt == '-s':
1550 if self.separator != None:
1551 self.usage('Only one of -c, -S and -s may be specified')
1552 return 1
1553 self.separator = ' '
1554 elif opt == '-d':
1555 self.print_designator = 1
1557 # if no command - go interactive
1558 # wrap in a try/finally so we always close off the db
1559 ret = 0
1560 try:
1561 if not args:
1562 self.interactive()
1563 else:
1564 ret = self.run_command(args)
1565 if self.db: self.db.commit()
1566 return ret
1567 finally:
1568 if self.db:
1569 self.db.close()
1571 if __name__ == '__main__':
1572 tool = AdminTool()
1573 sys.exit(tool.main())
1575 # vim: set filetype=python sts=4 sw=4 et si :