Mercurial > p > roundup > code
comparison roundup-admin @ 301:7901b58cfae8
Oops, committed the admin script with the wierd #! line.
Also, made the thing into a class to reduce parameter passing.
Nuked the leading whitespace from the help __doc__ displays too.
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Thu, 18 Oct 2001 02:16:42 +0000 |
| parents | fd9835c1e58d |
| children | d1fb3fcdb11b |
comparison
equal
deleted
inserted
replaced
| 300:590abb8e808c | 301:7901b58cfae8 |
|---|---|
| 1 #! /Users/builder/bin/python | 1 #! /usr/bin/env python |
| 2 # | 2 # |
| 3 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/) | 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 | 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 | 5 # under the same terms as Python, so long as this copyright message and |
| 6 # disclaimer are retained in their original form. | 6 # disclaimer are retained in their original form. |
| 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.33 2001-10-17 23:13:19 richard Exp $ | 19 # $Id: roundup-admin,v 1.34 2001-10-18 02:16:42 richard Exp $ |
| 20 | 20 |
| 21 import sys | 21 import sys |
| 22 if int(sys.version[0]) < 2: | 22 if int(sys.version[0]) < 2: |
| 23 print 'Roundup requires python 2.0 or later.' | 23 print 'Roundup requires python 2.0 or later.' |
| 24 sys.exit(1) | 24 sys.exit(1) |
| 29 except ImportError: | 29 except ImportError: |
| 30 csv = None | 30 csv = None |
| 31 from roundup import date, hyperdb, roundupdb, init, password | 31 from roundup import date, hyperdb, roundupdb, init, password |
| 32 import roundup.instance | 32 import roundup.instance |
| 33 | 33 |
| 34 def usage(message=''): | 34 class AdminTool: |
| 35 if message: message = 'Problem: '+message+'\n' | 35 |
| 36 print '''%sUsage: roundup-admin [-i instance home] [-u login] [-c] <command> <arguments> | 36 def __init__(self): |
| 37 self.commands = {} | |
| 38 for k, v in AdminTool.__dict__.items(): | |
| 39 if k[:3] == 'do_': | |
| 40 self.commands[k[3:]] = v | |
| 41 self.help = {} | |
| 42 for k, v in AdminTool.__dict__.items(): | |
| 43 if k[:5] == 'help_': | |
| 44 self.help[k[5:]] = v | |
| 45 | |
| 46 def usage(message=''): | |
| 47 if message: message = 'Problem: '+message+'\n' | |
| 48 print '''%sUsage: roundup-admin [-i instance home] [-u login] [-c] <command> <arguments> | |
| 37 | 49 |
| 38 Help: | 50 Help: |
| 39 roundup-admin -h | 51 roundup-admin -h |
| 40 roundup-admin help -- this help | 52 roundup-admin help -- this help |
| 41 roundup-admin help <command> -- command-specific help | 53 roundup-admin help <command> -- command-specific help |
| 42 roundup-admin help all -- all available help | 54 roundup-admin help all -- all available help |
| 43 Options: | 55 Options: |
| 44 -i instance home -- specify the issue tracker "home directory" to administer | 56 -i instance home -- specify the issue tracker "home directory" to administer |
| 45 -u -- the user[:password] to use for commands | 57 -u -- the user[:password] to use for commands |
| 46 -c -- when outputting lists of data, just comma-separate them'''%message | 58 -c -- when outputting lists of data, just comma-separate them'''%message |
| 47 help_commands() | 59 self.help_commands() |
| 48 | 60 |
| 49 def help_commands(): | 61 def help_commands(self): |
| 50 print 'Commands:', | 62 print 'Commands:', |
| 51 commands = [''] | 63 commands = [''] |
| 52 for command in figureCommands().values(): | 64 for command in self.commands.values(): |
| 53 h = command.__doc__.split('\n')[0] | 65 h = command.__doc__.split('\n')[0] |
| 54 commands.append(h[7:]) | 66 commands.append(h[7:]) |
| 55 commands.sort() | 67 commands.sort() |
| 56 print '\n '.join(commands) | 68 print '\n '.join(commands) |
| 57 | 69 |
| 58 def help_all(): | 70 def help_all(self): |
| 59 print ''' | 71 print ''' |
| 60 All commands (except help) require an instance specifier. This is just the path | 72 All commands (except help) require an instance specifier. This is just the path |
| 61 to the roundup instance you're working with. A roundup instance is where | 73 to the roundup instance you're working with. A roundup instance is where |
| 62 roundup keeps the database and configuration file that defines an issue | 74 roundup keeps the database and configuration file that defines an issue |
| 63 tracker. It may be thought of as the issue tracker's "home directory". It may | 75 tracker. It may be thought of as the issue tracker's "home directory". It may |
| 64 be specified in the environment variable ROUNDUP_INSTANCE or on the command | 76 be specified in the environment variable ROUNDUP_INSTANCE or on the command |
| 103 "8:47:11" means <Date yyyy-mm-dd.13:47:11> | 115 "8:47:11" means <Date yyyy-mm-dd.13:47:11> |
| 104 "." means "right now" | 116 "." means "right now" |
| 105 | 117 |
| 106 Command help: | 118 Command help: |
| 107 ''' | 119 ''' |
| 108 for name, command in figureCommands().items(): | 120 for name, command in self.commands.items(): |
| 109 print '%s:'%name | 121 print '%s:'%name |
| 110 print ' ',command.__doc__ | 122 print ' ',command.__doc__ |
| 111 | 123 |
| 112 def do_help(args): | 124 def do_help(self, args, nl_re=re.compile('[\r\n]'), |
| 113 '''Usage: help topic | 125 indent_re=re.compile(r'^(\s+)\S+')): |
| 114 Give help about topic. | 126 '''Usage: help topic |
| 115 | 127 Give help about topic. |
| 116 commands -- list commands | 128 |
| 117 <command> -- help specific to a command | 129 commands -- list commands |
| 118 initopts -- init command options | 130 <command> -- help specific to a command |
| 119 all -- all available help | 131 initopts -- init command options |
| 120 ''' | 132 all -- all available help |
| 121 help = figureHelp().get(args[0], None) | 133 ''' |
| 122 if help: | 134 help = self.help.get(args[0], None) |
| 123 help() | 135 if help: |
| 124 return | 136 help(self) |
| 125 help = figureCommands().get(args[0], None) | 137 return |
| 126 if help: | 138 help = self.commands.get(args[0], None) |
| 127 print help.__doc__ | 139 if help: |
| 128 else: | 140 # display the help, removing the docsring indent |
| 129 print 'Sorry, no help for "%s"'%args[0] | 141 lines = nl_re.split(help.__doc__) |
| 130 | 142 print lines[0] |
| 131 def help_initopts(): | 143 indent = indent_re.match(lines[1]) |
| 132 import roundup.templates | 144 if indent: indent = len(indent.group(1)) |
| 133 templates = roundup.templates.listTemplates() | 145 for line in lines[1:]: |
| 134 print 'Templates:', ', '.join(templates) | 146 if indent: |
| 135 import roundup.backends | 147 print line[indent:] |
| 136 backends = roundup.backends.__all__ | 148 else: |
| 137 print 'Back ends:', ', '.join(backends) | 149 print line |
| 138 | 150 else: |
| 139 | 151 print 'Sorry, no help for "%s"'%args[0] |
| 140 def do_init(instance_home, args, comma_sep=0): | 152 |
| 141 '''Usage: init [template [backend [admin password]]] | 153 def help_initopts(self): |
| 142 Initialise a new Roundup instance. | 154 import roundup.templates |
| 143 | 155 templates = roundup.templates.listTemplates() |
| 144 The command will prompt for the instance home directory (if not supplied | |
| 145 through INSTANCE_HOME or the -i option. The template, backend and admin | |
| 146 password may be specified on the command-line as arguments, in that order. | |
| 147 | |
| 148 See also initopts help. | |
| 149 ''' | |
| 150 # select template | |
| 151 import roundup.templates | |
| 152 templates = roundup.templates.listTemplates() | |
| 153 template = len(args) > 1 and args[1] or '' | |
| 154 if template not in templates: | |
| 155 print 'Templates:', ', '.join(templates) | 156 print 'Templates:', ', '.join(templates) |
| 156 while template not in templates: | 157 import roundup.backends |
| 157 template = raw_input('Select template [extended]: ').strip() | 158 backends = roundup.backends.__all__ |
| 158 if not template: | |
| 159 template = 'extended' | |
| 160 | |
| 161 import roundup.backends | |
| 162 backends = roundup.backends.__all__ | |
| 163 backend = len(args) > 2 and args[2] or '' | |
| 164 if backend not in backends: | |
| 165 print 'Back ends:', ', '.join(backends) | 159 print 'Back ends:', ', '.join(backends) |
| 166 while backend not in backends: | 160 |
| 167 backend = raw_input('Select backend [anydbm]: ').strip() | 161 |
| 168 if not backend: | 162 def do_init(instance_home, args): |
| 169 backend = 'anydbm' | 163 '''Usage: init [template [backend [admin password]]] |
| 170 if len(args) > 3: | 164 Initialise a new Roundup instance. |
| 171 adminpw = confirm = args[3] | 165 |
| 172 else: | 166 The command will prompt for the instance home directory (if not supplied |
| 173 adminpw = '' | 167 through INSTANCE_HOME or the -i option. The template, backend and admin |
| 174 confirm = 'x' | 168 password may be specified on the command-line as arguments, in that |
| 175 while adminpw != confirm: | 169 order. |
| 176 adminpw = getpass.getpass('Admin Password: ') | 170 |
| 177 confirm = getpass.getpass(' Confirm: ') | 171 See also initopts help. |
| 178 init.init(instance_home, template, backend, adminpw) | 172 ''' |
| 179 return 0 | 173 # select template |
| 180 | 174 import roundup.templates |
| 181 | 175 templates = roundup.templates.listTemplates() |
| 182 def do_get(db, args, comma_sep=0): | 176 template = len(args) > 1 and args[1] or '' |
| 183 '''Usage: get property designator[,designator]* | 177 if template not in templates: |
| 184 Get the given property of one or more designator(s). | 178 print 'Templates:', ', '.join(templates) |
| 185 | 179 while template not in templates: |
| 186 Retrieves the property value of the nodes specified by the designators. | 180 template = raw_input('Select template [extended]: ').strip() |
| 187 ''' | 181 if not template: |
| 188 propname = args[0] | 182 template = 'extended' |
| 189 designators = string.split(args[1], ',') | 183 |
| 190 l = [] | 184 import roundup.backends |
| 191 for designator in designators: | 185 backends = roundup.backends.__all__ |
| 192 classname, nodeid = roundupdb.splitDesignator(designator) | 186 backend = len(args) > 2 and args[2] or '' |
| 193 if comma_sep: | 187 if backend not in backends: |
| 194 l.append(db.getclass(classname).get(nodeid, propname)) | 188 print 'Back ends:', ', '.join(backends) |
| 189 while backend not in backends: | |
| 190 backend = raw_input('Select backend [anydbm]: ').strip() | |
| 191 if not backend: | |
| 192 backend = 'anydbm' | |
| 193 if len(args) > 3: | |
| 194 adminpw = confirm = args[3] | |
| 195 else: | 195 else: |
| 196 print db.getclass(classname).get(nodeid, propname) | 196 adminpw = '' |
| 197 if comma_sep: | 197 confirm = 'x' |
| 198 print ','.join(l) | 198 while adminpw != confirm: |
| 199 return 0 | 199 adminpw = getpass.getpass('Admin Password: ') |
| 200 | 200 confirm = getpass.getpass(' Confirm: ') |
| 201 | 201 init.init(instance_home, template, backend, adminpw) |
| 202 def do_set(db, args, comma_sep=0): | 202 return 0 |
| 203 '''Usage: set designator[,designator]* propname=value ... | 203 |
| 204 Set the given property of one or more designator(s). | 204 |
| 205 | 205 def do_get(self, args): |
| 206 Sets the property to the value for all designators given. | 206 '''Usage: get property designator[,designator]* |
| 207 ''' | 207 Get the given property of one or more designator(s). |
| 208 from roundup import hyperdb | 208 |
| 209 | 209 Retrieves the property value of the nodes specified by the designators. |
| 210 designators = string.split(args[0], ',') | 210 ''' |
| 211 props = {} | 211 propname = args[0] |
| 212 for prop in args[1:]: | 212 designators = string.split(args[1], ',') |
| 213 key, value = prop.split('=') | 213 l = [] |
| 214 props[key] = value | 214 for designator in designators: |
| 215 for designator in designators: | 215 classname, nodeid = roundupdb.splitDesignator(designator) |
| 216 classname, nodeid = roundupdb.splitDesignator(designator) | 216 if self.comma_sep: |
| 217 cl = db.getclass(classname) | 217 l.append(self.db.getclass(classname).get(nodeid, propname)) |
| 218 properties = cl.getprops() | 218 else: |
| 219 for key, value in props.items(): | 219 print self.db.getclass(classname).get(nodeid, propname) |
| 220 if self.comma_sep: | |
| 221 print ','.join(l) | |
| 222 return 0 | |
| 223 | |
| 224 | |
| 225 def do_set(self, args): | |
| 226 '''Usage: set designator[,designator]* propname=value ... | |
| 227 Set the given property of one or more designator(s). | |
| 228 | |
| 229 Sets the property to the value for all designators given. | |
| 230 ''' | |
| 231 from roundup import hyperdb | |
| 232 | |
| 233 designators = string.split(args[0], ',') | |
| 234 props = {} | |
| 235 for prop in args[1:]: | |
| 236 key, value = prop.split('=') | |
| 237 props[key] = value | |
| 238 for designator in designators: | |
| 239 classname, nodeid = roundupdb.splitDesignator(designator) | |
| 240 cl = self.db.getclass(classname) | |
| 241 properties = cl.getprops() | |
| 242 for key, value in props.items(): | |
| 243 type = properties[key] | |
| 244 if isinstance(type, hyperdb.String): | |
| 245 continue | |
| 246 elif isinstance(type, hyperdb.Password): | |
| 247 props[key] = password.Password(value) | |
| 248 elif isinstance(type, hyperdb.Date): | |
| 249 props[key] = date.Date(value) | |
| 250 elif isinstance(type, hyperdb.Interval): | |
| 251 props[key] = date.Interval(value) | |
| 252 elif isinstance(type, hyperdb.Link): | |
| 253 props[key] = value | |
| 254 elif isinstance(type, hyperdb.Multilink): | |
| 255 props[key] = value.split(',') | |
| 256 apply(cl.set, (nodeid, ), props) | |
| 257 return 0 | |
| 258 | |
| 259 def do_find(self, args): | |
| 260 '''Usage: find classname propname=value ... | |
| 261 Find the nodes of the given class with a given link property value. | |
| 262 | |
| 263 Find the nodes of the given class with a given link property value. The | |
| 264 value may be either the nodeid of the linked node, or its key value. | |
| 265 ''' | |
| 266 classname = args[0] | |
| 267 cl = self.db.getclass(classname) | |
| 268 | |
| 269 # look up the linked-to class and get the nodeid that has the value | |
| 270 propname, value = args[1].split('=') | |
| 271 num_re = re.compile('^\d+$') | |
| 272 if not num_re.match(value): | |
| 273 propcl = cl.properties[propname] | |
| 274 if (not isinstance(propcl, hyperdb.Link) and not | |
| 275 isinstance(type, hyperdb.Multilink)): | |
| 276 print 'You may only "find" link properties' | |
| 277 return 1 | |
| 278 propcl = self.db.getclass(propcl.classname) | |
| 279 value = propcl.lookup(value) | |
| 280 | |
| 281 # now do the find | |
| 282 if self.comma_sep: | |
| 283 print ','.join(cl.find(**{propname: value})) | |
| 284 else: | |
| 285 print cl.find(**{propname: value}) | |
| 286 return 0 | |
| 287 | |
| 288 def do_spec(self, args): | |
| 289 '''Usage: spec classname | |
| 290 Show the properties for a classname. | |
| 291 | |
| 292 This lists the properties for a given class. | |
| 293 ''' | |
| 294 classname = args[0] | |
| 295 cl = self.db.getclass(classname) | |
| 296 keyprop = cl.getkey() | |
| 297 for key, value in cl.properties.items(): | |
| 298 if keyprop == key: | |
| 299 print '%s: %s (key property)'%(key, value) | |
| 300 else: | |
| 301 print '%s: %s'%(key, value) | |
| 302 | |
| 303 def do_create(self, args): | |
| 304 '''Usage: create classname property=value ... | |
| 305 Create a new entry of a given class. | |
| 306 | |
| 307 This creates a new entry of the given class using the property | |
| 308 name=value arguments provided on the command line after the "create" | |
| 309 command. | |
| 310 ''' | |
| 311 from roundup import hyperdb | |
| 312 | |
| 313 classname = args[0] | |
| 314 cl = self.db.getclass(classname) | |
| 315 props = {} | |
| 316 properties = cl.getprops(protected = 0) | |
| 317 if len(args) == 1: | |
| 318 # ask for the properties | |
| 319 for key, value in properties.items(): | |
| 320 if key == 'id': continue | |
| 321 name = value.__class__.__name__ | |
| 322 if isinstance(value , hyperdb.Password): | |
| 323 again = None | |
| 324 while value != again: | |
| 325 value = getpass.getpass('%s (Password): '%key.capitalize()) | |
| 326 again = getpass.getpass(' %s (Again): '%key.capitalize()) | |
| 327 if value != again: print 'Sorry, try again...' | |
| 328 if value: | |
| 329 props[key] = value | |
| 330 else: | |
| 331 value = raw_input('%s (%s): '%(key.capitalize(), name)) | |
| 332 if value: | |
| 333 props[key] = value | |
| 334 else: | |
| 335 # use the args | |
| 336 for prop in args[1:]: | |
| 337 key, value = prop.split('=') | |
| 338 props[key] = value | |
| 339 | |
| 340 # convert types | |
| 341 for key in props.keys(): | |
| 220 type = properties[key] | 342 type = properties[key] |
| 221 if isinstance(type, hyperdb.String): | 343 if isinstance(type, hyperdb.Date): |
| 222 continue | |
| 223 elif isinstance(type, hyperdb.Password): | |
| 224 props[key] = password.Password(value) | |
| 225 elif isinstance(type, hyperdb.Date): | |
| 226 props[key] = date.Date(value) | 344 props[key] = date.Date(value) |
| 227 elif isinstance(type, hyperdb.Interval): | 345 elif isinstance(type, hyperdb.Interval): |
| 228 props[key] = date.Interval(value) | 346 props[key] = date.Interval(value) |
| 229 elif isinstance(type, hyperdb.Link): | 347 elif isinstance(type, hyperdb.Password): |
| 230 props[key] = value | 348 props[key] = password.Password(value) |
| 231 elif isinstance(type, hyperdb.Multilink): | 349 elif isinstance(type, hyperdb.Multilink): |
| 232 props[key] = value.split(',') | 350 props[key] = value.split(',') |
| 233 apply(cl.set, (nodeid, ), props) | 351 |
| 234 return 0 | 352 if cl.getkey() and not props.has_key(cl.getkey()): |
| 235 | 353 print "You must provide the '%s' property."%cl.getkey() |
| 236 def do_find(db, args, comma_sep=0): | |
| 237 '''Usage: find classname propname=value ... | |
| 238 Find the nodes of the given class with a given link property value. | |
| 239 | |
| 240 Find the nodes of the given class with a given link property value. The | |
| 241 value may be either the nodeid of the linked node, or its key value. | |
| 242 ''' | |
| 243 classname = args[0] | |
| 244 cl = db.getclass(classname) | |
| 245 | |
| 246 # look up the linked-to class and get the nodeid that has the value | |
| 247 propname, value = args[1].split('=') | |
| 248 num_re = re.compile('^\d+$') | |
| 249 if not num_re.match(value): | |
| 250 propcl = cl.properties[propname] | |
| 251 if (not isinstance(propcl, hyperdb.Link) and not | |
| 252 isinstance(type, hyperdb.Multilink)): | |
| 253 print 'You may only "find" link properties' | |
| 254 return 1 | |
| 255 propcl = db.getclass(propcl.classname) | |
| 256 value = propcl.lookup(value) | |
| 257 | |
| 258 # now do the find | |
| 259 if comma_sep: | |
| 260 print ','.join(cl.find(**{propname: value})) | |
| 261 else: | |
| 262 print cl.find(**{propname: value}) | |
| 263 return 0 | |
| 264 | |
| 265 def do_spec(db, args, comma_sep=0): | |
| 266 '''Usage: spec classname | |
| 267 Show the properties for a classname. | |
| 268 | |
| 269 This lists the properties for a given class. | |
| 270 ''' | |
| 271 classname = args[0] | |
| 272 cl = db.getclass(classname) | |
| 273 keyprop = cl.getkey() | |
| 274 for key, value in cl.properties.items(): | |
| 275 if keyprop == key: | |
| 276 print '%s: %s (key property)'%(key, value) | |
| 277 else: | 354 else: |
| 278 print '%s: %s'%(key, value) | 355 print apply(cl.create, (), props) |
| 279 | 356 |
| 280 def do_create(db, args, comma_sep=0): | 357 return 0 |
| 281 '''Usage: create classname property=value ... | 358 |
| 282 Create a new entry of a given class. | 359 def do_list(self, args): |
| 283 | 360 '''Usage: list classname [property] |
| 284 This creates a new entry of the given class using the property | 361 List the instances of a class. |
| 285 name=value arguments provided on the command line after the "create" | 362 |
| 286 command. | 363 Lists all instances of the given class. If the property is not |
| 287 ''' | 364 specified, the "label" property is used. The label property is tried |
| 288 from roundup import hyperdb | 365 in order: the key, "name", "title" and then the first property, |
| 289 | 366 alphabetically. |
| 290 classname = args[0] | 367 ''' |
| 291 cl = db.getclass(classname) | 368 classname = args[0] |
| 292 props = {} | 369 cl = self.db.getclass(classname) |
| 293 properties = cl.getprops(protected = 0) | 370 if len(args) > 1: |
| 294 if len(args) == 1: | 371 key = args[1] |
| 295 # ask for the properties | 372 else: |
| 296 for key, value in properties.items(): | 373 key = cl.labelprop() |
| 297 if key == 'id': continue | 374 if self.comma_sep: |
| 298 name = value.__class__.__name__ | 375 print ','.join(cl.list()) |
| 299 if isinstance(value , hyperdb.Password): | 376 else: |
| 300 again = None | 377 for nodeid in cl.list(): |
| 301 while value != again: | 378 value = cl.get(nodeid, key) |
| 302 value = getpass.getpass('%s (Password): '%key.capitalize()) | 379 print "%4s: %s"%(nodeid, value) |
| 303 again = getpass.getpass(' %s (Again): '%key.capitalize()) | 380 return 0 |
| 304 if value != again: print 'Sorry, try again...' | 381 |
| 305 if value: | 382 def do_table(self, args): |
| 306 props[key] = value | 383 '''Usage: table classname [property[,property]*] |
| 384 List the instances of a class in tabular form. | |
| 385 | |
| 386 Lists all instances of the given class. If the properties are not | |
| 387 specified, all properties are displayed. By default, the column widths | |
| 388 are the width of the property names. The width may be explicitly defined | |
| 389 by defining the property as "name:width". For example:: | |
| 390 roundup> table priority id,name:10 | |
| 391 Id Name | |
| 392 1 fatal-bug | |
| 393 2 bug | |
| 394 3 usability | |
| 395 4 feature | |
| 396 ''' | |
| 397 classname = args[0] | |
| 398 cl = self.db.getclass(classname) | |
| 399 if len(args) > 1: | |
| 400 prop_names = args[1].split(',') | |
| 401 else: | |
| 402 prop_names = cl.getprops().keys() | |
| 403 props = [] | |
| 404 for name in prop_names: | |
| 405 if ':' in name: | |
| 406 name, width = name.split(':') | |
| 407 props.append((name, int(width))) | |
| 307 else: | 408 else: |
| 308 value = raw_input('%s (%s): '%(key.capitalize(), name)) | 409 props.append((name, len(name))) |
| 309 if value: | 410 |
| 310 props[key] = value | 411 print ' '.join([string.capitalize(name) for name, width in props]) |
| 311 else: | |
| 312 # use the args | |
| 313 for prop in args[1:]: | |
| 314 key, value = prop.split('=') | |
| 315 props[key] = value | |
| 316 | |
| 317 # convert types | |
| 318 for key in props.keys(): | |
| 319 type = properties[key] | |
| 320 if isinstance(type, hyperdb.Date): | |
| 321 props[key] = date.Date(value) | |
| 322 elif isinstance(type, hyperdb.Interval): | |
| 323 props[key] = date.Interval(value) | |
| 324 elif isinstance(type, hyperdb.Password): | |
| 325 props[key] = password.Password(value) | |
| 326 elif isinstance(type, hyperdb.Multilink): | |
| 327 props[key] = value.split(',') | |
| 328 | |
| 329 if cl.getkey() and not props.has_key(cl.getkey()): | |
| 330 print "You must provide the '%s' property."%cl.getkey() | |
| 331 else: | |
| 332 print apply(cl.create, (), props) | |
| 333 | |
| 334 return 0 | |
| 335 | |
| 336 def do_list(db, args, comma_sep=0): | |
| 337 '''Usage: list classname [property] | |
| 338 List the instances of a class. | |
| 339 | |
| 340 Lists all instances of the given class. If the property is not | |
| 341 specified, the "label" property is used. The label property is tried | |
| 342 in order: the key, "name", "title" and then the first property, | |
| 343 alphabetically. | |
| 344 ''' | |
| 345 classname = args[0] | |
| 346 cl = db.getclass(classname) | |
| 347 if len(args) > 1: | |
| 348 key = args[1] | |
| 349 else: | |
| 350 key = cl.labelprop() | |
| 351 if comma_sep: | |
| 352 print ','.join(cl.list()) | |
| 353 else: | |
| 354 for nodeid in cl.list(): | |
| 355 value = cl.get(nodeid, key) | |
| 356 print "%4s: %s"%(nodeid, value) | |
| 357 return 0 | |
| 358 | |
| 359 def do_table(db, args, comma_sep=None): | |
| 360 '''Usage: table classname [property[,property]*] | |
| 361 List the instances of a class in tabular form. | |
| 362 | |
| 363 Lists all instances of the given class. If the properties are not | |
| 364 specified, all properties are displayed. By default, the column widths | |
| 365 are the width of the property names. The width may be explicitly defined | |
| 366 by defining the property as "name:width". For example:: | |
| 367 roundup> table priority id,name:10 | |
| 368 Id Name | |
| 369 1 fatal-bug | |
| 370 2 bug | |
| 371 3 usability | |
| 372 4 feature | |
| 373 ''' | |
| 374 classname = args[0] | |
| 375 cl = db.getclass(classname) | |
| 376 if len(args) > 1: | |
| 377 prop_names = args[1].split(',') | |
| 378 else: | |
| 379 prop_names = cl.getprops().keys() | |
| 380 props = [] | |
| 381 for name in prop_names: | |
| 382 if ':' in name: | |
| 383 name, width = name.split(':') | |
| 384 props.append((name, int(width))) | |
| 385 else: | |
| 386 props.append((name, len(name))) | |
| 387 | |
| 388 print ' '.join([string.capitalize(name) for name, width in props]) | |
| 389 for nodeid in cl.list(): | |
| 390 l = [] | |
| 391 for name, width in props: | |
| 392 if name != 'id': | |
| 393 value = str(cl.get(nodeid, name)) | |
| 394 else: | |
| 395 value = str(nodeid) | |
| 396 f = '%%-%ds'%width | |
| 397 l.append(f%value[:width]) | |
| 398 print ' '.join(l) | |
| 399 return 0 | |
| 400 | |
| 401 def do_history(db, args, comma_sep=0): | |
| 402 '''Usage: history designator | |
| 403 Show the history entries of a designator. | |
| 404 | |
| 405 Lists the journal entries for the node identified by the designator. | |
| 406 ''' | |
| 407 classname, nodeid = roundupdb.splitDesignator(args[0]) | |
| 408 # TODO: handle the -c option? | |
| 409 print db.getclass(classname).history(nodeid) | |
| 410 return 0 | |
| 411 | |
| 412 def do_retire(db, args, comma_sep=0): | |
| 413 '''Usage: retire designator[,designator]* | |
| 414 Retire the node specified by designator. | |
| 415 | |
| 416 This action indicates that a particular node is not to be retrieved by | |
| 417 the list or find commands, and its key value may be re-used. | |
| 418 ''' | |
| 419 designators = string.split(args[0], ',') | |
| 420 for designator in designators: | |
| 421 classname, nodeid = roundupdb.splitDesignator(designator) | |
| 422 db.getclass(classname).retire(nodeid) | |
| 423 return 0 | |
| 424 | |
| 425 def do_export(db, args, comma_sep=0): | |
| 426 '''Usage: export class[,class] destination_dir | |
| 427 Export the database to tab-separated-value files. | |
| 428 | |
| 429 This action exports the current data from the database into | |
| 430 tab-separated-value files that are placed in the nominated destination | |
| 431 directory. The journals are not exported. | |
| 432 ''' | |
| 433 if len(args) < 2: | |
| 434 print do_export.__doc__ | |
| 435 return 1 | |
| 436 classes = string.split(args[0], ',') | |
| 437 dir = args[1] | |
| 438 | |
| 439 # use the csv parser if we can - it's faster | |
| 440 if csv is not None: | |
| 441 p = csv.parser(field_sep=':') | |
| 442 | |
| 443 # do all the classes specified | |
| 444 for classname in classes: | |
| 445 cl = db.getclass(classname) | |
| 446 f = open(os.path.join(dir, classname+'.csv'), 'w') | |
| 447 f.write(string.join(cl.properties.keys(), ':') + '\n') | |
| 448 | |
| 449 # all nodes for this class | |
| 450 properties = cl.properties.items() | |
| 451 for nodeid in cl.list(): | 412 for nodeid in cl.list(): |
| 452 l = [] | 413 l = [] |
| 453 for prop, type in properties: | 414 for name, width in props: |
| 454 value = cl.get(nodeid, prop) | 415 if name != 'id': |
| 455 # convert data where needed | 416 value = str(cl.get(nodeid, name)) |
| 417 else: | |
| 418 value = str(nodeid) | |
| 419 f = '%%-%ds'%width | |
| 420 l.append(f%value[:width]) | |
| 421 print ' '.join(l) | |
| 422 return 0 | |
| 423 | |
| 424 def do_history(self, args): | |
| 425 '''Usage: history designator | |
| 426 Show the history entries of a designator. | |
| 427 | |
| 428 Lists the journal entries for the node identified by the designator. | |
| 429 ''' | |
| 430 classname, nodeid = roundupdb.splitDesignator(args[0]) | |
| 431 # TODO: handle the -c option? | |
| 432 print self.db.getclass(classname).history(nodeid) | |
| 433 return 0 | |
| 434 | |
| 435 def do_retire(self, args): | |
| 436 '''Usage: retire designator[,designator]* | |
| 437 Retire the node specified by designator. | |
| 438 | |
| 439 This action indicates that a particular node is not to be retrieved by | |
| 440 the list or find commands, and its key value may be re-used. | |
| 441 ''' | |
| 442 designators = string.split(args[0], ',') | |
| 443 for designator in designators: | |
| 444 classname, nodeid = roundupdb.splitDesignator(designator) | |
| 445 self.db.getclass(classname).retire(nodeid) | |
| 446 return 0 | |
| 447 | |
| 448 def do_export(self, args): | |
| 449 '''Usage: export class[,class] destination_dir | |
| 450 Export the database to tab-separated-value files. | |
| 451 | |
| 452 This action exports the current data from the database into | |
| 453 tab-separated-value files that are placed in the nominated destination | |
| 454 directory. The journals are not exported. | |
| 455 ''' | |
| 456 if len(args) < 2: | |
| 457 print do_export.__doc__ | |
| 458 return 1 | |
| 459 classes = string.split(args[0], ',') | |
| 460 dir = args[1] | |
| 461 | |
| 462 # use the csv parser if we can - it's faster | |
| 463 if csv is not None: | |
| 464 p = csv.parser(field_sep=':') | |
| 465 | |
| 466 # do all the classes specified | |
| 467 for classname in classes: | |
| 468 cl = self.db.getclass(classname) | |
| 469 f = open(os.path.join(dir, classname+'.csv'), 'w') | |
| 470 f.write(string.join(cl.properties.keys(), ':') + '\n') | |
| 471 | |
| 472 # all nodes for this class | |
| 473 properties = cl.properties.items() | |
| 474 for nodeid in cl.list(): | |
| 475 l = [] | |
| 476 for prop, type in properties: | |
| 477 value = cl.get(nodeid, prop) | |
| 478 # convert data where needed | |
| 479 if isinstance(type, hyperdb.Date): | |
| 480 value = value.get_tuple() | |
| 481 elif isinstance(type, hyperdb.Interval): | |
| 482 value = value.get_tuple() | |
| 483 elif isinstance(type, hyperdb.Password): | |
| 484 value = str(value) | |
| 485 l.append(repr(value)) | |
| 486 | |
| 487 # now write | |
| 488 if csv is not None: | |
| 489 f.write(p.join(l) + '\n') | |
| 490 else: | |
| 491 # escape the individual entries to they're valid CSV | |
| 492 m = [] | |
| 493 for entry in l: | |
| 494 if '"' in entry: | |
| 495 entry = '""'.join(entry.split('"')) | |
| 496 if ':' in entry: | |
| 497 entry = '"%s"'%entry | |
| 498 m.append(entry) | |
| 499 f.write(':'.join(m) + '\n') | |
| 500 return 0 | |
| 501 | |
| 502 def do_import(self, args): | |
| 503 '''Usage: import class file | |
| 504 Import the contents of the tab-separated-value file. | |
| 505 | |
| 506 The file must define the same properties as the class (including having | |
| 507 a "header" line with those property names.) The new nodes are added to | |
| 508 the existing database - if you want to create a new database using the | |
| 509 imported data, then create a new database (or, tediously, retire all | |
| 510 the old data.) | |
| 511 ''' | |
| 512 if len(args) < 2: | |
| 513 print do_import.__doc__ | |
| 514 return 1 | |
| 515 if csv is None: | |
| 516 print 'Sorry, you need the csv module to use this function.' | |
| 517 print 'Get it from: http://www.object-craft.com.au/projects/csv/' | |
| 518 return 1 | |
| 519 | |
| 520 from roundup import hyperdb | |
| 521 | |
| 522 # ensure that the properties and the CSV file headings match | |
| 523 cl = self.db.getclass(args[0]) | |
| 524 f = open(args[1]) | |
| 525 p = csv.parser(field_sep=':') | |
| 526 file_props = p.parse(f.readline()) | |
| 527 props = cl.properties.keys() | |
| 528 m = file_props[:] | |
| 529 m.sort() | |
| 530 props.sort() | |
| 531 if m != props: | |
| 532 print 'Import file doesn\'t define the same properties as "%s".'%args[0] | |
| 533 return 1 | |
| 534 | |
| 535 # loop through the file and create a node for each entry | |
| 536 n = range(len(props)) | |
| 537 while 1: | |
| 538 line = f.readline() | |
| 539 if not line: break | |
| 540 | |
| 541 # parse lines until we get a complete entry | |
| 542 while 1: | |
| 543 l = p.parse(line) | |
| 544 if l: break | |
| 545 | |
| 546 # make the new node's property map | |
| 547 d = {} | |
| 548 for i in n: | |
| 549 # Use eval to reverse the repr() used to output the CSV | |
| 550 value = eval(l[i]) | |
| 551 # Figure the property for this column | |
| 552 key = file_props[i] | |
| 553 type = cl.properties[key] | |
| 554 # Convert for property type | |
| 456 if isinstance(type, hyperdb.Date): | 555 if isinstance(type, hyperdb.Date): |
| 457 value = value.get_tuple() | 556 value = date.Date(value) |
| 458 elif isinstance(type, hyperdb.Interval): | 557 elif isinstance(type, hyperdb.Interval): |
| 459 value = value.get_tuple() | 558 value = date.Interval(value) |
| 460 elif isinstance(type, hyperdb.Password): | 559 elif isinstance(type, hyperdb.Password): |
| 461 value = str(value) | 560 pwd = password.Password() |
| 462 l.append(repr(value)) | 561 pwd.unpack(value) |
| 463 | 562 value = pwd |
| 464 # now write | 563 if value is not None: |
| 465 if csv is not None: | 564 d[key] = value |
| 466 f.write(p.join(l) + '\n') | 565 |
| 467 else: | 566 # and create the new node |
| 468 # escape the individual entries to they're valid CSV | 567 apply(cl.create, (), d) |
| 469 m = [] | 568 return 0 |
| 470 for entry in l: | 569 |
| 471 if '"' in entry: | |
| 472 entry = '""'.join(entry.split('"')) | |
| 473 if ':' in entry: | |
| 474 entry = '"%s"'%entry | |
| 475 m.append(entry) | |
| 476 f.write(':'.join(m) + '\n') | |
| 477 return 0 | |
| 478 | |
| 479 def do_import(db, args, comma_sep=0): | |
| 480 '''Usage: import class file | |
| 481 Import the contents of the tab-separated-value file. | |
| 482 | |
| 483 The file must define the same properties as the class (including having | |
| 484 a "header" line with those property names.) The new nodes are added to | |
| 485 the existing database - if you want to create a new database using the | |
| 486 imported data, then create a new database (or, tediously, retire all | |
| 487 the old data.) | |
| 488 ''' | |
| 489 if len(args) < 2: | |
| 490 print do_import.__doc__ | |
| 491 return 1 | |
| 492 if csv is None: | |
| 493 print 'Sorry, you need the csv module to use this function.' | |
| 494 print 'Get it from: http://www.object-craft.com.au/projects/csv/' | |
| 495 return 1 | |
| 496 | |
| 497 from roundup import hyperdb | |
| 498 | |
| 499 # ensure that the properties and the CSV file headings match | |
| 500 cl = db.getclass(args[0]) | |
| 501 f = open(args[1]) | |
| 502 p = csv.parser(field_sep=':') | |
| 503 file_props = p.parse(f.readline()) | |
| 504 props = cl.properties.keys() | |
| 505 m = file_props[:] | |
| 506 m.sort() | |
| 507 props.sort() | |
| 508 if m != props: | |
| 509 print 'Import file doesn\'t define the same properties as "%s".'%args[0] | |
| 510 return 1 | |
| 511 | |
| 512 # loop through the file and create a node for each entry | |
| 513 n = range(len(props)) | |
| 514 while 1: | |
| 515 line = f.readline() | |
| 516 if not line: break | |
| 517 | |
| 518 # parse lines until we get a complete entry | |
| 519 while 1: | |
| 520 l = p.parse(line) | |
| 521 if l: break | |
| 522 | |
| 523 # make the new node's property map | |
| 524 d = {} | |
| 525 for i in n: | |
| 526 # Use eval to reverse the repr() used to output the CSV | |
| 527 value = eval(l[i]) | |
| 528 # Figure the property for this column | |
| 529 key = file_props[i] | |
| 530 type = cl.properties[key] | |
| 531 # Convert for property type | |
| 532 if isinstance(type, hyperdb.Date): | |
| 533 value = date.Date(value) | |
| 534 elif isinstance(type, hyperdb.Interval): | |
| 535 value = date.Interval(value) | |
| 536 elif isinstance(type, hyperdb.Password): | |
| 537 pwd = password.Password() | |
| 538 pwd.unpack(value) | |
| 539 value = pwd | |
| 540 if value is not None: | |
| 541 d[key] = value | |
| 542 | |
| 543 # and create the new node | |
| 544 apply(cl.create, (), d) | |
| 545 return 0 | |
| 546 | |
| 547 def figureCommands(): | |
| 548 d = {} | |
| 549 for k, v in globals().items(): | |
| 550 if k[:3] == 'do_': | |
| 551 d[k[3:]] = v | |
| 552 return d | |
| 553 | |
| 554 def figureHelp(): | |
| 555 d = {} | |
| 556 for k, v in globals().items(): | |
| 557 if k[:5] == 'help_': | |
| 558 d[k[5:]] = v | |
| 559 return d | |
| 560 | |
| 561 class AdminTool: | |
| 562 def run_command(self, args): | 570 def run_command(self, args): |
| 563 '''Run a single command | 571 '''Run a single command |
| 564 ''' | 572 ''' |
| 565 command = args[0] | 573 command = args[0] |
| 566 | 574 |
| 567 # handle help now | 575 # handle help now |
| 568 if command == 'help': | 576 if command == 'help': |
| 569 if len(args)>1: | 577 if len(args)>1: |
| 570 do_help(args[1:]) | 578 self.do_help(args[1:]) |
| 571 return 0 | 579 return 0 |
| 572 do_help(['help']) | 580 self.do_help(['help']) |
| 573 return 0 | 581 return 0 |
| 574 if command == 'morehelp': | 582 if command == 'morehelp': |
| 575 do_help(['help']) | 583 self.do_help(['help']) |
| 576 help_commands() | 584 self.help_commands() |
| 577 help_all() | 585 self.help_all() |
| 578 return 0 | 586 return 0 |
| 579 | 587 |
| 580 # make sure we have an instance_home | 588 # make sure we have an instance_home |
| 581 while not self.instance_home: | 589 while not self.instance_home: |
| 582 self.instance_home = raw_input('Enter instance home: ').strip() | 590 self.instance_home = raw_input('Enter instance home: ').strip() |
| 583 | 591 |
| 584 # before we open the db, we may be doing an init | 592 # before we open the db, we may be doing an init |
| 585 if command == 'init': | 593 if command == 'init': |
| 586 return do_init(self.instance_home, args) | 594 return self.do_init(self.instance_home, args) |
| 587 | 595 |
| 588 function = figureCommands().get(command, None) | 596 function = self.commands.get(command, None) |
| 589 | 597 |
| 590 # not a valid command | 598 # not a valid command |
| 591 if function is None: | 599 if function is None: |
| 592 print 'Unknown command "%s" ("help commands" for a list)'%command | 600 print 'Unknown command "%s" ("help commands" for a list)'%command |
| 593 return 1 | 601 return 1 |
| 594 | 602 |
| 595 # get the instance | 603 # get the instance |
| 596 instance = roundup.instance.open(self.instance_home) | 604 instance = roundup.instance.open(self.instance_home) |
| 597 db = instance.open('admin') | 605 self.db = instance.open('admin') |
| 598 | 606 |
| 599 if len(args) < 2: | 607 if len(args) < 2: |
| 600 print function.__doc__ | 608 print function.__doc__ |
| 601 return 1 | 609 return 1 |
| 602 | 610 |
| 603 # do the command | 611 # do the command |
| 604 try: | 612 try: |
| 605 return function(db, args[1:], comma_sep=self.comma_sep) | 613 return function(args[1:]) |
| 606 finally: | 614 finally: |
| 607 db.close() | 615 self.db.close() |
| 608 | 616 |
| 609 return 1 | 617 return 1 |
| 610 | 618 |
| 611 def interactive(self, ws_re=re.compile(r'\s+')): | 619 def interactive(self, ws_re=re.compile(r'\s+')): |
| 612 '''Run in an interactive mode | 620 '''Run in an interactive mode |
| 661 tool = AdminTool() | 669 tool = AdminTool() |
| 662 sys.exit(tool.main()) | 670 sys.exit(tool.main()) |
| 663 | 671 |
| 664 # | 672 # |
| 665 # $Log: not supported by cvs2svn $ | 673 # $Log: not supported by cvs2svn $ |
| 674 # Revision 1.33 2001/10/17 23:13:19 richard | |
| 675 # Did a fair bit of work on the admin tool. Now has an extra command "table" | |
| 676 # which displays node information in a tabular format. Also fixed import and | |
| 677 # export so they work. Removed freshen. | |
| 678 # Fixed quopri usage in mailgw from bug reports. | |
| 679 # | |
| 666 # Revision 1.32 2001/10/17 06:57:29 richard | 680 # Revision 1.32 2001/10/17 06:57:29 richard |
| 667 # Interactive startup blurb - need to figure how to get the version in there. | 681 # Interactive startup blurb - need to figure how to get the version in there. |
| 668 # | 682 # |
| 669 # Revision 1.31 2001/10/17 06:17:26 richard | 683 # Revision 1.31 2001/10/17 06:17:26 richard |
| 670 # Now with readline support :) | 684 # Now with readline support :) |
