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