Mercurial > p > roundup > code
comparison roundup-admin @ 484:b35f229dd049
I18N'ed roundup admin...
...and split the code off into a module so it can be used elsewhere.
Big issue with this is the doc strings - that's the help. We're
probably going to have to switch to not use docstrings, which will
suck a little :(
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Sat, 05 Jan 2002 02:11:22 +0000 |
| parents | ef06a66a0b72 |
| children | 18d4051bdae7 |
comparison
equal
deleted
inserted
replaced
| 483:a090b3873d82 | 484:b35f229dd049 |
|---|---|
| 14 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS | 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" | 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, | 16 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, |
| 17 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. | 17 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. |
| 18 # | 18 # |
| 19 # $Id: roundup-admin,v 1.59 2001-12-31 05:20:34 richard Exp $ | 19 # $Id: roundup-admin,v 1.60 2002-01-05 02:11:22 richard Exp $ |
| 20 | 20 |
| 21 # python version check | 21 # python version check |
| 22 from roundup import version_check | 22 from roundup import version_check |
| 23 | 23 |
| 24 import sys, os, getpass, getopt, re, UserDict, shlex | 24 # import the admin tool guts and make it go |
| 25 try: | 25 from roundup.admin import AdminTool |
| 26 import csv | |
| 27 except ImportError: | |
| 28 csv = None | |
| 29 from roundup import date, hyperdb, roundupdb, init, password, token | |
| 30 import roundup.instance | |
| 31 | |
| 32 class CommandDict(UserDict.UserDict): | |
| 33 '''Simple dictionary that lets us do lookups using partial keys. | |
| 34 | |
| 35 Original code submitted by Engelbert Gruber. | |
| 36 ''' | |
| 37 _marker = [] | |
| 38 def get(self, key, default=_marker): | |
| 39 if self.data.has_key(key): | |
| 40 return [(key, self.data[key])] | |
| 41 keylist = self.data.keys() | |
| 42 keylist.sort() | |
| 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 | |
| 50 | |
| 51 class UsageError(ValueError): | |
| 52 pass | |
| 53 | |
| 54 class AdminTool: | |
| 55 | |
| 56 def __init__(self): | |
| 57 self.commands = CommandDict() | |
| 58 for k in AdminTool.__dict__.keys(): | |
| 59 if k[:3] == 'do_': | |
| 60 self.commands[k[3:]] = getattr(self, k) | |
| 61 self.help = {} | |
| 62 for k in AdminTool.__dict__.keys(): | |
| 63 if k[:5] == 'help_': | |
| 64 self.help[k[5:]] = getattr(self, k) | |
| 65 self.instance_home = '' | |
| 66 self.db = None | |
| 67 | |
| 68 def usage(self, message=''): | |
| 69 if message: message = 'Problem: '+message+'\n\n' | |
| 70 print '''%sUsage: roundup-admin [-i instance home] [-u login] [-c] <command> <arguments> | |
| 71 | |
| 72 Help: | |
| 73 roundup-admin -h | |
| 74 roundup-admin help -- this help | |
| 75 roundup-admin help <command> -- command-specific help | |
| 76 roundup-admin help all -- all available help | |
| 77 Options: | |
| 78 -i instance home -- specify the issue tracker "home directory" to administer | |
| 79 -u -- the user[:password] to use for commands | |
| 80 -c -- when outputting lists of data, just comma-separate them'''%message | |
| 81 self.help_commands() | |
| 82 | |
| 83 def help_commands(self): | |
| 84 print 'Commands:', | |
| 85 commands = [''] | |
| 86 for command in self.commands.values(): | |
| 87 h = command.__doc__.split('\n')[0] | |
| 88 commands.append(' '+h[7:]) | |
| 89 commands.sort() | |
| 90 commands.append( | |
| 91 'Commands may be abbreviated as long as the abbreviation matches only one') | |
| 92 commands.append('command, e.g. l == li == lis == list.') | |
| 93 print '\n'.join(commands) | |
| 94 print | |
| 95 | |
| 96 def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')): | |
| 97 commands = self.commands.values() | |
| 98 def sortfun(a, b): | |
| 99 return cmp(a.__name__, b.__name__) | |
| 100 commands.sort(sortfun) | |
| 101 for command in commands: | |
| 102 h = command.__doc__.split('\n') | |
| 103 name = command.__name__[3:] | |
| 104 usage = h[0] | |
| 105 print ''' | |
| 106 <tr><td valign=top><strong>%(name)s</strong></td> | |
| 107 <td><tt>%(usage)s</tt><p> | |
| 108 <pre>'''%locals() | |
| 109 indent = indent_re.match(h[3]) | |
| 110 if indent: indent = len(indent.group(1)) | |
| 111 for line in h[3:]: | |
| 112 if indent: | |
| 113 print line[indent:] | |
| 114 else: | |
| 115 print line | |
| 116 print '</pre></td></tr>\n' | |
| 117 | |
| 118 def help_all(self): | |
| 119 print ''' | |
| 120 All commands (except help) require an instance specifier. This is just the path | |
| 121 to the roundup instance you're working with. A roundup instance is where | |
| 122 roundup keeps the database and configuration file that defines an issue | |
| 123 tracker. It may be thought of as the issue tracker's "home directory". It may | |
| 124 be specified in the environment variable ROUNDUP_INSTANCE or on the command | |
| 125 line as "-i instance". | |
| 126 | |
| 127 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ... | |
| 128 | |
| 129 Property values are represented as strings in command arguments and in the | |
| 130 printed results: | |
| 131 . Strings are, well, strings. | |
| 132 . Date values are printed in the full date format in the local time zone, and | |
| 133 accepted in the full format or any of the partial formats explained below. | |
| 134 . Link values are printed as node designators. When given as an argument, | |
| 135 node designators and key strings are both accepted. | |
| 136 . Multilink values are printed as lists of node designators joined by commas. | |
| 137 When given as an argument, node designators and key strings are both | |
| 138 accepted; an empty string, a single node, or a list of nodes joined by | |
| 139 commas is accepted. | |
| 140 | |
| 141 When property values must contain spaces, just surround the value with | |
| 142 quotes, either ' or ". A single space may also be backslash-quoted. If a | |
| 143 valuu must contain a quote character, it must be backslash-quoted or inside | |
| 144 quotes. Examples: | |
| 145 hello world (2 tokens: hello, world) | |
| 146 "hello world" (1 token: hello world) | |
| 147 "Roch'e" Compaan (2 tokens: Roch'e Compaan) | |
| 148 Roch\'e Compaan (2 tokens: Roch'e Compaan) | |
| 149 address="1 2 3" (1 token: address=1 2 3) | |
| 150 \\ (1 token: \) | |
| 151 \n\r\t (1 token: a newline, carriage-return and tab) | |
| 152 | |
| 153 When multiple nodes are specified to the roundup get or roundup set | |
| 154 commands, the specified properties are retrieved or set on all the listed | |
| 155 nodes. | |
| 156 | |
| 157 When multiple results are returned by the roundup get or roundup find | |
| 158 commands, they are printed one per line (default) or joined by commas (with | |
| 159 the -c) option. | |
| 160 | |
| 161 Where the command changes data, a login name/password is required. The | |
| 162 login may be specified as either "name" or "name:password". | |
| 163 . ROUNDUP_LOGIN environment variable | |
| 164 . the -u command-line option | |
| 165 If either the name or password is not supplied, they are obtained from the | |
| 166 command-line. | |
| 167 | |
| 168 Date format examples: | |
| 169 "2000-04-17.03:45" means <Date 2000-04-17.08:45:00> | |
| 170 "2000-04-17" means <Date 2000-04-17.00:00:00> | |
| 171 "01-25" means <Date yyyy-01-25.00:00:00> | |
| 172 "08-13.22:13" means <Date yyyy-08-14.03:13:00> | |
| 173 "11-07.09:32:43" means <Date yyyy-11-07.14:32:43> | |
| 174 "14:25" means <Date yyyy-mm-dd.19:25:00> | |
| 175 "8:47:11" means <Date yyyy-mm-dd.13:47:11> | |
| 176 "." means "right now" | |
| 177 | |
| 178 Command help: | |
| 179 ''' | |
| 180 for name, command in self.commands.items(): | |
| 181 print '%s:'%name | |
| 182 print ' ',command.__doc__ | |
| 183 | |
| 184 def do_help(self, args, nl_re=re.compile('[\r\n]'), | |
| 185 indent_re=re.compile(r'^(\s+)\S+')): | |
| 186 '''Usage: help topic | |
| 187 Give help about topic. | |
| 188 | |
| 189 commands -- list commands | |
| 190 <command> -- help specific to a command | |
| 191 initopts -- init command options | |
| 192 all -- all available help | |
| 193 ''' | |
| 194 topic = args[0] | |
| 195 | |
| 196 # try help_ methods | |
| 197 if self.help.has_key(topic): | |
| 198 self.help[topic]() | |
| 199 return 0 | |
| 200 | |
| 201 # try command docstrings | |
| 202 try: | |
| 203 l = self.commands.get(topic) | |
| 204 except KeyError: | |
| 205 print 'Sorry, no help for "%s"'%topic | |
| 206 return 1 | |
| 207 | |
| 208 # display the help for each match, removing the docsring indent | |
| 209 for name, help in l: | |
| 210 lines = nl_re.split(help.__doc__) | |
| 211 print lines[0] | |
| 212 indent = indent_re.match(lines[1]) | |
| 213 if indent: indent = len(indent.group(1)) | |
| 214 for line in lines[1:]: | |
| 215 if indent: | |
| 216 print line[indent:] | |
| 217 else: | |
| 218 print line | |
| 219 return 0 | |
| 220 | |
| 221 def help_initopts(self): | |
| 222 import roundup.templates | |
| 223 templates = roundup.templates.listTemplates() | |
| 224 print 'Templates:', ', '.join(templates) | |
| 225 import roundup.backends | |
| 226 backends = roundup.backends.__all__ | |
| 227 print 'Back ends:', ', '.join(backends) | |
| 228 | |
| 229 | |
| 230 def do_initialise(self, instance_home, args): | |
| 231 '''Usage: initialise [template [backend [admin password]]] | |
| 232 Initialise a new Roundup instance. | |
| 233 | |
| 234 The command will prompt for the instance home directory (if not supplied | |
| 235 through INSTANCE_HOME or the -i option. The template, backend and admin | |
| 236 password may be specified on the command-line as arguments, in that | |
| 237 order. | |
| 238 | |
| 239 See also initopts help. | |
| 240 ''' | |
| 241 if len(args) < 1: | |
| 242 raise UsageError, 'Not enough arguments supplied' | |
| 243 # select template | |
| 244 import roundup.templates | |
| 245 templates = roundup.templates.listTemplates() | |
| 246 template = len(args) > 1 and args[1] or '' | |
| 247 if template not in templates: | |
| 248 print 'Templates:', ', '.join(templates) | |
| 249 while template not in templates: | |
| 250 template = raw_input('Select template [classic]: ').strip() | |
| 251 if not template: | |
| 252 template = 'classic' | |
| 253 | |
| 254 import roundup.backends | |
| 255 backends = roundup.backends.__all__ | |
| 256 backend = len(args) > 2 and args[2] or '' | |
| 257 if backend not in backends: | |
| 258 print 'Back ends:', ', '.join(backends) | |
| 259 while backend not in backends: | |
| 260 backend = raw_input('Select backend [anydbm]: ').strip() | |
| 261 if not backend: | |
| 262 backend = 'anydbm' | |
| 263 if len(args) > 3: | |
| 264 adminpw = confirm = args[3] | |
| 265 else: | |
| 266 adminpw = '' | |
| 267 confirm = 'x' | |
| 268 while adminpw != confirm: | |
| 269 adminpw = getpass.getpass('Admin Password: ') | |
| 270 confirm = getpass.getpass(' Confirm: ') | |
| 271 init.init(instance_home, template, backend, adminpw) | |
| 272 return 0 | |
| 273 | |
| 274 | |
| 275 def do_get(self, args): | |
| 276 '''Usage: get property designator[,designator]* | |
| 277 Get the given property of one or more designator(s). | |
| 278 | |
| 279 Retrieves the property value of the nodes specified by the designators. | |
| 280 ''' | |
| 281 if len(args) < 2: | |
| 282 raise UsageError, 'Not enough arguments supplied' | |
| 283 propname = args[0] | |
| 284 designators = args[1].split(',') | |
| 285 l = [] | |
| 286 for designator in designators: | |
| 287 # decode the node designator | |
| 288 try: | |
| 289 classname, nodeid = roundupdb.splitDesignator(designator) | |
| 290 except roundupdb.DesignatorError, message: | |
| 291 raise UsageError, message | |
| 292 | |
| 293 # get the class | |
| 294 try: | |
| 295 cl = self.db.getclass(classname) | |
| 296 except KeyError: | |
| 297 raise UsageError, 'invalid class "%s"'%classname | |
| 298 try: | |
| 299 if self.comma_sep: | |
| 300 l.append(cl.get(nodeid, propname)) | |
| 301 else: | |
| 302 print cl.get(nodeid, propname) | |
| 303 except IndexError: | |
| 304 raise UsageError, 'no such %s node "%s"'%(classname, nodeid) | |
| 305 except KeyError: | |
| 306 raise UsageError, 'no such %s property "%s"'%(classname, | |
| 307 propname) | |
| 308 if self.comma_sep: | |
| 309 print ','.join(l) | |
| 310 return 0 | |
| 311 | |
| 312 | |
| 313 def do_set(self, args): | |
| 314 '''Usage: set designator[,designator]* propname=value ... | |
| 315 Set the given property of one or more designator(s). | |
| 316 | |
| 317 Sets the property to the value for all designators given. | |
| 318 ''' | |
| 319 if len(args) < 2: | |
| 320 raise UsageError, 'Not enough arguments supplied' | |
| 321 from roundup import hyperdb | |
| 322 | |
| 323 designators = args[0].split(',') | |
| 324 props = {} | |
| 325 for prop in args[1:]: | |
| 326 if prop.find('=') == -1: | |
| 327 raise UsageError, 'argument "%s" not propname=value'%prop | |
| 328 try: | |
| 329 key, value = prop.split('=') | |
| 330 except ValueError: | |
| 331 raise UsageError, 'argument "%s" not propname=value'%prop | |
| 332 props[key] = value | |
| 333 for designator in designators: | |
| 334 # decode the node designator | |
| 335 try: | |
| 336 classname, nodeid = roundupdb.splitDesignator(designator) | |
| 337 except roundupdb.DesignatorError, message: | |
| 338 raise UsageError, message | |
| 339 | |
| 340 # get the class | |
| 341 try: | |
| 342 cl = self.db.getclass(classname) | |
| 343 except KeyError: | |
| 344 raise UsageError, 'invalid class "%s"'%classname | |
| 345 | |
| 346 properties = cl.getprops() | |
| 347 for key, value in props.items(): | |
| 348 proptype = properties[key] | |
| 349 if isinstance(proptype, hyperdb.String): | |
| 350 continue | |
| 351 elif isinstance(proptype, hyperdb.Password): | |
| 352 props[key] = password.Password(value) | |
| 353 elif isinstance(proptype, hyperdb.Date): | |
| 354 try: | |
| 355 props[key] = date.Date(value) | |
| 356 except ValueError, message: | |
| 357 raise UsageError, '"%s": %s'%(value, message) | |
| 358 elif isinstance(proptype, hyperdb.Interval): | |
| 359 try: | |
| 360 props[key] = date.Interval(value) | |
| 361 except ValueError, message: | |
| 362 raise UsageError, '"%s": %s'%(value, message) | |
| 363 elif isinstance(proptype, hyperdb.Link): | |
| 364 props[key] = value | |
| 365 elif isinstance(proptype, hyperdb.Multilink): | |
| 366 props[key] = value.split(',') | |
| 367 | |
| 368 # try the set | |
| 369 try: | |
| 370 apply(cl.set, (nodeid, ), props) | |
| 371 except (TypeError, IndexError, ValueError), message: | |
| 372 raise UsageError, message | |
| 373 return 0 | |
| 374 | |
| 375 def do_find(self, args): | |
| 376 '''Usage: find classname propname=value ... | |
| 377 Find the nodes of the given class with a given link property value. | |
| 378 | |
| 379 Find the nodes of the given class with a given link property value. The | |
| 380 value may be either the nodeid of the linked node, or its key value. | |
| 381 ''' | |
| 382 if len(args) < 1: | |
| 383 raise UsageError, 'Not enough arguments supplied' | |
| 384 classname = args[0] | |
| 385 # get the class | |
| 386 try: | |
| 387 cl = self.db.getclass(classname) | |
| 388 except KeyError: | |
| 389 raise UsageError, 'invalid class "%s"'%classname | |
| 390 | |
| 391 # TODO: handle > 1 argument | |
| 392 # handle the propname=value argument | |
| 393 if args[1].find('=') == -1: | |
| 394 raise UsageError, 'argument "%s" not propname=value'%prop | |
| 395 try: | |
| 396 propname, value = args[1].split('=') | |
| 397 except ValueError: | |
| 398 raise UsageError, 'argument "%s" not propname=value'%prop | |
| 399 | |
| 400 # if the value isn't a number, look up the linked class to get the | |
| 401 # number | |
| 402 num_re = re.compile('^\d+$') | |
| 403 if not num_re.match(value): | |
| 404 # get the property | |
| 405 try: | |
| 406 property = cl.properties[propname] | |
| 407 except KeyError: | |
| 408 raise UsageError, '%s has no property "%s"'%(classname, | |
| 409 propname) | |
| 410 | |
| 411 # make sure it's a link | |
| 412 if (not isinstance(property, hyperdb.Link) and not | |
| 413 isinstance(property, hyperdb.Multilink)): | |
| 414 raise UsageError, 'You may only "find" link properties' | |
| 415 | |
| 416 # get the linked-to class and look up the key property | |
| 417 link_class = self.db.getclass(property.classname) | |
| 418 try: | |
| 419 value = link_class.lookup(value) | |
| 420 except TypeError: | |
| 421 raise UsageError, '%s has no key property"'%link_class.classname | |
| 422 except KeyError: | |
| 423 raise UsageError, '%s has no entry "%s"'%(link_class.classname, | |
| 424 propname) | |
| 425 | |
| 426 # now do the find | |
| 427 try: | |
| 428 if self.comma_sep: | |
| 429 print ','.join(apply(cl.find, (), {propname: value})) | |
| 430 else: | |
| 431 print apply(cl.find, (), {propname: value}) | |
| 432 except KeyError: | |
| 433 raise UsageError, '%s has no property "%s"'%(classname, | |
| 434 propname) | |
| 435 except (ValueError, TypeError), message: | |
| 436 raise UsageError, message | |
| 437 return 0 | |
| 438 | |
| 439 def do_specification(self, args): | |
| 440 '''Usage: specification classname | |
| 441 Show the properties for a classname. | |
| 442 | |
| 443 This lists the properties for a given class. | |
| 444 ''' | |
| 445 if len(args) < 1: | |
| 446 raise UsageError, 'Not enough arguments supplied' | |
| 447 classname = args[0] | |
| 448 # get the class | |
| 449 try: | |
| 450 cl = self.db.getclass(classname) | |
| 451 except KeyError: | |
| 452 raise UsageError, 'invalid class "%s"'%classname | |
| 453 | |
| 454 # get the key property | |
| 455 keyprop = cl.getkey() | |
| 456 for key, value in cl.properties.items(): | |
| 457 if keyprop == key: | |
| 458 print '%s: %s (key property)'%(key, value) | |
| 459 else: | |
| 460 print '%s: %s'%(key, value) | |
| 461 | |
| 462 def do_display(self, args): | |
| 463 '''Usage: display designator | |
| 464 Show the property values for the given node. | |
| 465 | |
| 466 This lists the properties and their associated values for the given | |
| 467 node. | |
| 468 ''' | |
| 469 if len(args) < 1: | |
| 470 raise UsageError, 'Not enough arguments supplied' | |
| 471 | |
| 472 # decode the node designator | |
| 473 try: | |
| 474 classname, nodeid = roundupdb.splitDesignator(args[0]) | |
| 475 except roundupdb.DesignatorError, message: | |
| 476 raise UsageError, message | |
| 477 | |
| 478 # get the class | |
| 479 try: | |
| 480 cl = self.db.getclass(classname) | |
| 481 except KeyError: | |
| 482 raise UsageError, 'invalid class "%s"'%classname | |
| 483 | |
| 484 # display the values | |
| 485 for key in cl.properties.keys(): | |
| 486 value = cl.get(nodeid, key) | |
| 487 print '%s: %s'%(key, value) | |
| 488 | |
| 489 def do_create(self, args): | |
| 490 '''Usage: create classname property=value ... | |
| 491 Create a new entry of a given class. | |
| 492 | |
| 493 This creates a new entry of the given class using the property | |
| 494 name=value arguments provided on the command line after the "create" | |
| 495 command. | |
| 496 ''' | |
| 497 if len(args) < 1: | |
| 498 raise UsageError, 'Not enough arguments supplied' | |
| 499 from roundup import hyperdb | |
| 500 | |
| 501 classname = args[0] | |
| 502 | |
| 503 # get the class | |
| 504 try: | |
| 505 cl = self.db.getclass(classname) | |
| 506 except KeyError: | |
| 507 raise UsageError, 'invalid class "%s"'%classname | |
| 508 | |
| 509 # now do a create | |
| 510 props = {} | |
| 511 properties = cl.getprops(protected = 0) | |
| 512 if len(args) == 1: | |
| 513 # ask for the properties | |
| 514 for key, value in properties.items(): | |
| 515 if key == 'id': continue | |
| 516 name = value.__class__.__name__ | |
| 517 if isinstance(value , hyperdb.Password): | |
| 518 again = None | |
| 519 while value != again: | |
| 520 value = getpass.getpass('%s (Password): '%key.capitalize()) | |
| 521 again = getpass.getpass(' %s (Again): '%key.capitalize()) | |
| 522 if value != again: print 'Sorry, try again...' | |
| 523 if value: | |
| 524 props[key] = value | |
| 525 else: | |
| 526 value = raw_input('%s (%s): '%(key.capitalize(), name)) | |
| 527 if value: | |
| 528 props[key] = value | |
| 529 else: | |
| 530 # use the args | |
| 531 for prop in args[1:]: | |
| 532 if prop.find('=') == -1: | |
| 533 raise UsageError, 'argument "%s" not propname=value'%prop | |
| 534 try: | |
| 535 key, value = prop.split('=') | |
| 536 except ValueError: | |
| 537 raise UsageError, 'argument "%s" not propname=value'%prop | |
| 538 props[key] = value | |
| 539 | |
| 540 # convert types | |
| 541 for key in props.keys(): | |
| 542 # get the property | |
| 543 try: | |
| 544 proptype = properties[key] | |
| 545 except KeyError: | |
| 546 raise UsageError, '%s has no property "%s"'%(classname, key) | |
| 547 | |
| 548 if isinstance(proptype, hyperdb.Date): | |
| 549 try: | |
| 550 props[key] = date.Date(value) | |
| 551 except ValueError, message: | |
| 552 raise UsageError, '"%s": %s'%(value, message) | |
| 553 elif isinstance(proptype, hyperdb.Interval): | |
| 554 try: | |
| 555 props[key] = date.Interval(value) | |
| 556 except ValueError, message: | |
| 557 raise UsageError, '"%s": %s'%(value, message) | |
| 558 elif isinstance(proptype, hyperdb.Password): | |
| 559 props[key] = password.Password(value) | |
| 560 elif isinstance(proptype, hyperdb.Multilink): | |
| 561 props[key] = value.split(',') | |
| 562 | |
| 563 # check for the key property | |
| 564 if cl.getkey() and not props.has_key(cl.getkey()): | |
| 565 raise UsageError, "you must provide the '%s' property."%cl.getkey() | |
| 566 | |
| 567 # do the actual create | |
| 568 try: | |
| 569 print apply(cl.create, (), props) | |
| 570 except (TypeError, IndexError, ValueError), message: | |
| 571 raise UsageError, message | |
| 572 return 0 | |
| 573 | |
| 574 def do_list(self, args): | |
| 575 '''Usage: list classname [property] | |
| 576 List the instances of a class. | |
| 577 | |
| 578 Lists all instances of the given class. If the property is not | |
| 579 specified, the "label" property is used. The label property is tried | |
| 580 in order: the key, "name", "title" and then the first property, | |
| 581 alphabetically. | |
| 582 ''' | |
| 583 if len(args) < 1: | |
| 584 raise UsageError, 'Not enough arguments supplied' | |
| 585 classname = args[0] | |
| 586 | |
| 587 # get the class | |
| 588 try: | |
| 589 cl = self.db.getclass(classname) | |
| 590 except KeyError: | |
| 591 raise UsageError, 'invalid class "%s"'%classname | |
| 592 | |
| 593 # figure the property | |
| 594 if len(args) > 1: | |
| 595 key = args[1] | |
| 596 else: | |
| 597 key = cl.labelprop() | |
| 598 | |
| 599 if self.comma_sep: | |
| 600 print ','.join(cl.list()) | |
| 601 else: | |
| 602 for nodeid in cl.list(): | |
| 603 try: | |
| 604 value = cl.get(nodeid, key) | |
| 605 except KeyError: | |
| 606 raise UsageError, '%s has no property "%s"'%(classname, key) | |
| 607 print "%4s: %s"%(nodeid, value) | |
| 608 return 0 | |
| 609 | |
| 610 def do_table(self, args): | |
| 611 '''Usage: table classname [property[,property]*] | |
| 612 List the instances of a class in tabular form. | |
| 613 | |
| 614 Lists all instances of the given class. If the properties are not | |
| 615 specified, all properties are displayed. By default, the column widths | |
| 616 are the width of the property names. The width may be explicitly defined | |
| 617 by defining the property as "name:width". For example:: | |
| 618 roundup> table priority id,name:10 | |
| 619 Id Name | |
| 620 1 fatal-bug | |
| 621 2 bug | |
| 622 3 usability | |
| 623 4 feature | |
| 624 ''' | |
| 625 if len(args) < 1: | |
| 626 raise UsageError, 'Not enough arguments supplied' | |
| 627 classname = args[0] | |
| 628 | |
| 629 # get the class | |
| 630 try: | |
| 631 cl = self.db.getclass(classname) | |
| 632 except KeyError: | |
| 633 raise UsageError, 'invalid class "%s"'%classname | |
| 634 | |
| 635 # figure the property names to display | |
| 636 if len(args) > 1: | |
| 637 prop_names = args[1].split(',') | |
| 638 all_props = cl.getprops() | |
| 639 for spec in prop_names: | |
| 640 if ':' in spec: | |
| 641 try: | |
| 642 name, width = spec.split(':') | |
| 643 except (ValueError, TypeError): | |
| 644 raise UsageError, '"%s" not name:width'%spec | |
| 645 else: | |
| 646 name = spec | |
| 647 if not all_props.has_key(name): | |
| 648 raise UsageError, '%s has no property "%s"'%(classname, | |
| 649 name) | |
| 650 else: | |
| 651 prop_names = cl.getprops().keys() | |
| 652 | |
| 653 # now figure column widths | |
| 654 props = [] | |
| 655 for spec in prop_names: | |
| 656 if ':' in spec: | |
| 657 try: | |
| 658 name, width = spec.split(':') | |
| 659 except (ValueError, TypeError): | |
| 660 raise UsageError, '"%s" not name:width'%spec | |
| 661 props.append((name, int(width))) | |
| 662 else: | |
| 663 props.append((spec, len(spec))) | |
| 664 | |
| 665 # now display the heading | |
| 666 print ' '.join([name.capitalize().ljust(width) for name,width in props]) | |
| 667 | |
| 668 # and the table data | |
| 669 for nodeid in cl.list(): | |
| 670 l = [] | |
| 671 for name, width in props: | |
| 672 if name != 'id': | |
| 673 try: | |
| 674 value = str(cl.get(nodeid, name)) | |
| 675 except KeyError: | |
| 676 # we already checked if the property is valid - a | |
| 677 # KeyError here means the node just doesn't have a | |
| 678 # value for it | |
| 679 value = '' | |
| 680 else: | |
| 681 value = str(nodeid) | |
| 682 f = '%%-%ds'%width | |
| 683 l.append(f%value[:width]) | |
| 684 print ' '.join(l) | |
| 685 return 0 | |
| 686 | |
| 687 def do_history(self, args): | |
| 688 '''Usage: history designator | |
| 689 Show the history entries of a designator. | |
| 690 | |
| 691 Lists the journal entries for the node identified by the designator. | |
| 692 ''' | |
| 693 if len(args) < 1: | |
| 694 raise UsageError, 'Not enough arguments supplied' | |
| 695 try: | |
| 696 classname, nodeid = roundupdb.splitDesignator(args[0]) | |
| 697 except roundupdb.DesignatorError, message: | |
| 698 raise UsageError, message | |
| 699 | |
| 700 # TODO: handle the -c option? | |
| 701 try: | |
| 702 print self.db.getclass(classname).history(nodeid) | |
| 703 except KeyError: | |
| 704 raise UsageError, 'no such class "%s"'%classname | |
| 705 except IndexError: | |
| 706 raise UsageError, 'no such %s node "%s"'%(classname, nodeid) | |
| 707 return 0 | |
| 708 | |
| 709 def do_commit(self, args): | |
| 710 '''Usage: commit | |
| 711 Commit all changes made to the database. | |
| 712 | |
| 713 The changes made during an interactive session are not | |
| 714 automatically written to the database - they must be committed | |
| 715 using this command. | |
| 716 | |
| 717 One-off commands on the command-line are automatically committed if | |
| 718 they are successful. | |
| 719 ''' | |
| 720 self.db.commit() | |
| 721 return 0 | |
| 722 | |
| 723 def do_rollback(self, args): | |
| 724 '''Usage: rollback | |
| 725 Undo all changes that are pending commit to the database. | |
| 726 | |
| 727 The changes made during an interactive session are not | |
| 728 automatically written to the database - they must be committed | |
| 729 manually. This command undoes all those changes, so a commit | |
| 730 immediately after would make no changes to the database. | |
| 731 ''' | |
| 732 self.db.rollback() | |
| 733 return 0 | |
| 734 | |
| 735 def do_retire(self, args): | |
| 736 '''Usage: retire designator[,designator]* | |
| 737 Retire the node specified by designator. | |
| 738 | |
| 739 This action indicates that a particular node is not to be retrieved by | |
| 740 the list or find commands, and its key value may be re-used. | |
| 741 ''' | |
| 742 if len(args) < 1: | |
| 743 raise UsageError, 'Not enough arguments supplied' | |
| 744 designators = args[0].split(',') | |
| 745 for designator in designators: | |
| 746 try: | |
| 747 classname, nodeid = roundupdb.splitDesignator(designator) | |
| 748 except roundupdb.DesignatorError, message: | |
| 749 raise UsageError, message | |
| 750 try: | |
| 751 self.db.getclass(classname).retire(nodeid) | |
| 752 except KeyError: | |
| 753 raise UsageError, 'no such class "%s"'%classname | |
| 754 except IndexError: | |
| 755 raise UsageError, 'no such %s node "%s"'%(classname, nodeid) | |
| 756 return 0 | |
| 757 | |
| 758 def do_export(self, args): | |
| 759 '''Usage: export class[,class] destination_dir | |
| 760 Export the database to tab-separated-value files. | |
| 761 | |
| 762 This action exports the current data from the database into | |
| 763 tab-separated-value files that are placed in the nominated destination | |
| 764 directory. The journals are not exported. | |
| 765 ''' | |
| 766 if len(args) < 2: | |
| 767 raise UsageError, 'Not enough arguments supplied' | |
| 768 classes = args[0].split(',') | |
| 769 dir = args[1] | |
| 770 | |
| 771 # use the csv parser if we can - it's faster | |
| 772 if csv is not None: | |
| 773 p = csv.parser(field_sep=':') | |
| 774 | |
| 775 # do all the classes specified | |
| 776 for classname in classes: | |
| 777 try: | |
| 778 cl = self.db.getclass(classname) | |
| 779 except KeyError: | |
| 780 raise UsageError, 'no such class "%s"'%classname | |
| 781 f = open(os.path.join(dir, classname+'.csv'), 'w') | |
| 782 f.write(':'.join(cl.properties.keys()) + '\n') | |
| 783 | |
| 784 # all nodes for this class | |
| 785 properties = cl.properties.items() | |
| 786 for nodeid in cl.list(): | |
| 787 l = [] | |
| 788 for prop, proptype in properties: | |
| 789 value = cl.get(nodeid, prop) | |
| 790 # convert data where needed | |
| 791 if isinstance(proptype, hyperdb.Date): | |
| 792 value = value.get_tuple() | |
| 793 elif isinstance(proptype, hyperdb.Interval): | |
| 794 value = value.get_tuple() | |
| 795 elif isinstance(proptype, hyperdb.Password): | |
| 796 value = str(value) | |
| 797 l.append(repr(value)) | |
| 798 | |
| 799 # now write | |
| 800 if csv is not None: | |
| 801 f.write(p.join(l) + '\n') | |
| 802 else: | |
| 803 # escape the individual entries to they're valid CSV | |
| 804 m = [] | |
| 805 for entry in l: | |
| 806 if '"' in entry: | |
| 807 entry = '""'.join(entry.split('"')) | |
| 808 if ':' in entry: | |
| 809 entry = '"%s"'%entry | |
| 810 m.append(entry) | |
| 811 f.write(':'.join(m) + '\n') | |
| 812 return 0 | |
| 813 | |
| 814 def do_import(self, args): | |
| 815 '''Usage: import class file | |
| 816 Import the contents of the tab-separated-value file. | |
| 817 | |
| 818 The file must define the same properties as the class (including having | |
| 819 a "header" line with those property names.) The new nodes are added to | |
| 820 the existing database - if you want to create a new database using the | |
| 821 imported data, then create a new database (or, tediously, retire all | |
| 822 the old data.) | |
| 823 ''' | |
| 824 if len(args) < 2: | |
| 825 raise UsageError, 'Not enough arguments supplied' | |
| 826 if csv is None: | |
| 827 raise UsageError, \ | |
| 828 'Sorry, you need the csv module to use this function.\n'\ | |
| 829 'Get it from: http://www.object-craft.com.au/projects/csv/' | |
| 830 | |
| 831 from roundup import hyperdb | |
| 832 | |
| 833 # ensure that the properties and the CSV file headings match | |
| 834 classname = args[0] | |
| 835 try: | |
| 836 cl = self.db.getclass(classname) | |
| 837 except KeyError: | |
| 838 raise UsageError, 'no such class "%s"'%classname | |
| 839 f = open(args[1]) | |
| 840 p = csv.parser(field_sep=':') | |
| 841 file_props = p.parse(f.readline()) | |
| 842 props = cl.properties.keys() | |
| 843 m = file_props[:] | |
| 844 m.sort() | |
| 845 props.sort() | |
| 846 if m != props: | |
| 847 raise UsageError, 'Import file doesn\'t define the same '\ | |
| 848 'properties as "%s".'%args[0] | |
| 849 | |
| 850 # loop through the file and create a node for each entry | |
| 851 n = range(len(props)) | |
| 852 while 1: | |
| 853 line = f.readline() | |
| 854 if not line: break | |
| 855 | |
| 856 # parse lines until we get a complete entry | |
| 857 while 1: | |
| 858 l = p.parse(line) | |
| 859 if l: break | |
| 860 | |
| 861 # make the new node's property map | |
| 862 d = {} | |
| 863 for i in n: | |
| 864 # Use eval to reverse the repr() used to output the CSV | |
| 865 value = eval(l[i]) | |
| 866 # Figure the property for this column | |
| 867 key = file_props[i] | |
| 868 proptype = cl.properties[key] | |
| 869 # Convert for property type | |
| 870 if isinstance(proptype, hyperdb.Date): | |
| 871 value = date.Date(value) | |
| 872 elif isinstance(proptype, hyperdb.Interval): | |
| 873 value = date.Interval(value) | |
| 874 elif isinstance(proptype, hyperdb.Password): | |
| 875 pwd = password.Password() | |
| 876 pwd.unpack(value) | |
| 877 value = pwd | |
| 878 if value is not None: | |
| 879 d[key] = value | |
| 880 | |
| 881 # and create the new node | |
| 882 apply(cl.create, (), d) | |
| 883 return 0 | |
| 884 | |
| 885 def run_command(self, args): | |
| 886 '''Run a single command | |
| 887 ''' | |
| 888 command = args[0] | |
| 889 | |
| 890 # handle help now | |
| 891 if command == 'help': | |
| 892 if len(args)>1: | |
| 893 self.do_help(args[1:]) | |
| 894 return 0 | |
| 895 self.do_help(['help']) | |
| 896 return 0 | |
| 897 if command == 'morehelp': | |
| 898 self.do_help(['help']) | |
| 899 self.help_commands() | |
| 900 self.help_all() | |
| 901 return 0 | |
| 902 | |
| 903 # figure what the command is | |
| 904 try: | |
| 905 functions = self.commands.get(command) | |
| 906 except KeyError: | |
| 907 # not a valid command | |
| 908 print 'Unknown command "%s" ("help commands" for a list)'%command | |
| 909 return 1 | |
| 910 | |
| 911 # check for multiple matches | |
| 912 if len(functions) > 1: | |
| 913 print 'Multiple commands match "%s": %s'%(command, | |
| 914 ', '.join([i[0] for i in functions])) | |
| 915 return 1 | |
| 916 command, function = functions[0] | |
| 917 | |
| 918 # make sure we have an instance_home | |
| 919 while not self.instance_home: | |
| 920 self.instance_home = raw_input('Enter instance home: ').strip() | |
| 921 | |
| 922 # before we open the db, we may be doing an init | |
| 923 if command == 'initialise': | |
| 924 return self.do_initialise(self.instance_home, args) | |
| 925 | |
| 926 # get the instance | |
| 927 try: | |
| 928 instance = roundup.instance.open(self.instance_home) | |
| 929 except ValueError, message: | |
| 930 self.instance_home = '' | |
| 931 print "Couldn't open instance: %s"%message | |
| 932 return 1 | |
| 933 | |
| 934 # only open the database once! | |
| 935 if not self.db: | |
| 936 self.db = instance.open('admin') | |
| 937 | |
| 938 # do the command | |
| 939 ret = 0 | |
| 940 try: | |
| 941 ret = function(args[1:]) | |
| 942 except UsageError, message: | |
| 943 print 'Error: %s'%message | |
| 944 print function.__doc__ | |
| 945 ret = 1 | |
| 946 except: | |
| 947 import traceback | |
| 948 traceback.print_exc() | |
| 949 ret = 1 | |
| 950 return ret | |
| 951 | |
| 952 def interactive(self): | |
| 953 '''Run in an interactive mode | |
| 954 ''' | |
| 955 print 'Roundup {version} ready for input.' | |
| 956 print 'Type "help" for help.' | |
| 957 try: | |
| 958 import readline | |
| 959 except ImportError: | |
| 960 print "Note: command history and editing not available" | |
| 961 | |
| 962 while 1: | |
| 963 try: | |
| 964 command = raw_input('roundup> ') | |
| 965 except EOFError: | |
| 966 print 'exit...' | |
| 967 break | |
| 968 if not command: continue | |
| 969 args = token.token_split(command) | |
| 970 if not args: continue | |
| 971 if args[0] in ('quit', 'exit'): break | |
| 972 self.run_command(args) | |
| 973 | |
| 974 # exit.. check for transactions | |
| 975 if self.db and self.db.transactions: | |
| 976 commit = raw_input("There are unsaved changes. Commit them (y/N)? ") | |
| 977 if commit and commit[0].lower() == 'y': | |
| 978 self.db.commit() | |
| 979 return 0 | |
| 980 | |
| 981 def main(self): | |
| 982 try: | |
| 983 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc') | |
| 984 except getopt.GetoptError, e: | |
| 985 self.usage(str(e)) | |
| 986 return 1 | |
| 987 | |
| 988 # handle command-line args | |
| 989 self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '') | |
| 990 name = password = '' | |
| 991 if os.environ.has_key('ROUNDUP_LOGIN'): | |
| 992 l = os.environ['ROUNDUP_LOGIN'].split(':') | |
| 993 name = l[0] | |
| 994 if len(l) > 1: | |
| 995 password = l[1] | |
| 996 self.comma_sep = 0 | |
| 997 for opt, arg in opts: | |
| 998 if opt == '-h': | |
| 999 self.usage() | |
| 1000 return 0 | |
| 1001 if opt == '-i': | |
| 1002 self.instance_home = arg | |
| 1003 if opt == '-c': | |
| 1004 self.comma_sep = 1 | |
| 1005 | |
| 1006 # if no command - go interactive | |
| 1007 ret = 0 | |
| 1008 if not args: | |
| 1009 self.interactive() | |
| 1010 else: | |
| 1011 ret = self.run_command(args) | |
| 1012 if self.db: self.db.commit() | |
| 1013 return ret | |
| 1014 | |
| 1015 | |
| 1016 if __name__ == '__main__': | 26 if __name__ == '__main__': |
| 1017 tool = AdminTool() | 27 tool = AdminTool() |
| 1018 sys.exit(tool.main()) | 28 sys.exit(tool.main()) |
| 1019 | 29 |
| 1020 # | 30 # |
| 1021 # $Log: not supported by cvs2svn $ | 31 # $Log: not supported by cvs2svn $ |
| 32 # Revision 1.59 2001/12/31 05:20:34 richard | |
| 33 # . #496360 ] table width does not work | |
| 34 # | |
| 1022 # Revision 1.58 2001/12/31 05:12:52 richard | 35 # Revision 1.58 2001/12/31 05:12:52 richard |
| 1023 # actually handle the advertised <cr> response to "commit y/N?" | 36 # actually handle the advertised <cr> response to "commit y/N?" |
| 1024 # | 37 # |
| 1025 # Revision 1.57 2001/12/31 05:12:01 richard | 38 # Revision 1.57 2001/12/31 05:12:01 richard |
| 1026 # added some quoting instructions to roundup-admin | 39 # added some quoting instructions to roundup-admin |
