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

Roundup Issue Tracker: http://roundup-tracker.org/