comparison roundup/admin.py @ 6585:d4371b131c9c

flake 8 improvements Renamed some variables from l to something (I hope) more descriptive. Left a number of long lines and unindented quoted ''' string blocks. The blocks are often inside a _() translation function call. Adding white space to indent them would mess up the translation tables and require reformatting as they are output. I might be able to reformat: _( ''' message for the user here on multiple lines''' ) as separate concatenated lines and preserve the translation key but...
author John Rouillard <rouilj@ieee.org>
date Sun, 16 Jan 2022 12:54:26 -0500
parents 8687c096a945
children 408fd477761f
comparison
equal deleted inserted replaced
6584:770503bd211e 6585:d4371b131c9c
21 """ 21 """
22 from __future__ import print_function 22 from __future__ import print_function
23 23
24 __docformat__ = 'restructuredtext' 24 __docformat__ = 'restructuredtext'
25 25
26 import csv, getopt, getpass, os, re, shutil, sys, operator 26 import csv, getopt, getpass, operator, os, re, shutil, sys
27 27
28 from roundup import date, hyperdb, init, password, token 28 from roundup import date, hyperdb, init, password, token
29 from roundup import __version__ as roundup_version 29 from roundup import __version__ as roundup_version
30 import roundup.instance 30 import roundup.instance
31 from roundup.configuration import (CoreConfig, NoConfigError, 31 from roundup.configuration import (CoreConfig, NoConfigError,
50 50
51 def get(self, key, default=_marker): 51 def get(self, key, default=_marker):
52 if key in self.data: 52 if key in self.data:
53 return [(key, self.data[key])] 53 return [(key, self.data[key])]
54 keylist = sorted(self.data) 54 keylist = sorted(self.data)
55 l = [] 55 matching_keys = []
56 for ki in keylist: 56 for ki in keylist:
57 if ki.startswith(key): 57 if ki.startswith(key):
58 l.append((ki, self.data[ki])) 58 matching_keys.append((ki, self.data[ki]))
59 if not l and default is self._marker: 59 if not matching_keys and default is self._marker:
60 raise KeyError(key) 60 raise KeyError(key)
61 return l 61 # FIXME: what happens if default is not self._marker but
62 # there are no matching keys? Should (default, self.data[default])
63 # be returned???
64 return matching_keys
62 65
63 66
64 class AdminTool: 67 class AdminTool:
65 """ A collection of methods used in maintaining Roundup trackers. 68 """ A collection of methods used in maintaining Roundup trackers.
66 69
100 103
101 The args list is specified as ``prop=value prop=value ...``. 104 The args list is specified as ``prop=value prop=value ...``.
102 """ 105 """
103 props = {} 106 props = {}
104 for arg in args: 107 for arg in args:
105 l = arg.split('=', 1) 108 key_val = arg.split('=', 1)
106 # if = not in string, will return one element 109 # if = not in string, will return one element
107 if len(l) < 2: 110 if len(key_val) < 2:
108 raise UsageError(_('argument "%(arg)s" not propname=value') % 111 raise UsageError(_('argument "%(arg)s" not propname=value') %
109 locals()) 112 locals())
110 key, value = l 113 key, value = key_val
111 if value: 114 if value:
112 props[key] = value 115 props[key] = value
113 else: 116 else:
114 props[key] = None 117 props[key] = None
115 return props 118 return props
116 119
117 def usage(self, message=''): 120 def usage(self, message=''):
118 """ Display a simple usage message. 121 """ Display a simple usage message.
119 """ 122 """
120 if message: 123 if message:
121 message = _('Problem: %(message)s\n\n')% locals() 124 message = _('Problem: %(message)s\n\n') % locals()
122 sys.stdout.write(_("""%(message)sUsage: roundup-admin [options] [<command> <arguments>] 125 sys.stdout.write(_("""%(message)sUsage: roundup-admin [options] [<command> <arguments>]
123 126
124 Options: 127 Options:
125 -i instance home -- specify the issue tracker "home directory" to administer 128 -i instance home -- specify the issue tracker "home directory" to administer
126 -u -- the user[:password] to use for commands (default admin) 129 -u -- the user[:password] to use for commands (default admin)
266 self.help[topic]() 269 self.help[topic]()
267 return 0 270 return 0
268 271
269 # try command docstrings 272 # try command docstrings
270 try: 273 try:
271 l = self.commands.get(topic) 274 cmd_docs = self.commands.get(topic)
272 except KeyError: 275 except KeyError:
273 print(_('Sorry, no help for "%(topic)s"') % locals()) 276 print(_('Sorry, no help for "%(topic)s"') % locals())
274 return 1 277 return 1
275 278
276 # display the help for each match, removing the docstring indent 279 # display the help for each match, removing the docstring indent
277 for _name, help in l: 280 for _name, help in cmd_docs:
278 lines = nl_re.split(_(help.__doc__)) 281 lines = nl_re.split(_(help.__doc__))
279 print(lines[0]) 282 print(lines[0])
280 indent = indent_re.match(lines[1]) 283 indent = indent_re.match(lines[1])
281 if indent: indent = len(indent.group(1)) 284 if indent: indent = len(indent.group(1))
282 for line in lines[1:]: 285 for line in lines[1:]:
337 # __file__ should be something like: 340 # __file__ should be something like:
338 # /usr/local/lib/python3.10/site-packages/roundup/admin.py 341 # /usr/local/lib/python3.10/site-packages/roundup/admin.py
339 # os.prefix should be /usr, /usr/local or root of virtualenv 342 # os.prefix should be /usr, /usr/local or root of virtualenv
340 # strip leading / to make os.path.join work right. 343 # strip leading / to make os.path.join work right.
341 path = __file__ 344 path = __file__
342 for N in 1, 2: 345 for _N in 1, 2:
343 path = os.path.dirname(path) 346 path = os.path.dirname(path)
344 # path is /usr/local/lib/python3.10/site-packages 347 # path is /usr/local/lib/python3.10/site-packages
345 tdir = os.path.join(path, sys.prefix[1:], 'share', 348 tdir = os.path.join(path, sys.prefix[1:], 'share',
346 'roundup', 'templates') 349 'roundup', 'templates')
347 if os.path.isdir(tdir): 350 if os.path.isdir(tdir):
348 templates.update(init.listTemplates(tdir)) 351 templates.update(init.listTemplates(tdir))
349 352
350 # OK, now try as if we're in the roundup source distribution 353 # OK, now try as if we're in the roundup source distribution
351 # directory, so this module will be in .../roundup-*/roundup/admin.py 354 # directory, so this module will be in .../roundup-*/roundup/admin.py
352 # and we're interested in the .../roundup-*/ part. 355 # and we're interested in the .../roundup-*/ part.
353 path = __file__ 356 path = __file__
354 for _i in range(2): 357 for _i in range(2):
456 459
457 # load config_ini.ini from template if it exists. 460 # load config_ini.ini from template if it exists.
458 # it sets parameters like template_engine that are 461 # it sets parameters like template_engine that are
459 # template specific. 462 # template specific.
460 template_config = UserConfig(templates[template]['path'] + 463 template_config = UserConfig(templates[template]['path'] +
461 "/config_ini.ini") 464 "/config_ini.ini")
462 for k in template_config.keys(): 465 for k in template_config.keys():
463 if k == 'HOME': # ignore home. It is a default param. 466 if k == 'HOME': # ignore home. It is a default param.
464 continue 467 continue
465 defns[k] = template_config[k] 468 defns[k] = template_config[k]
466 469
604 """ 607 """
605 if len(args) < 2: 608 if len(args) < 2:
606 raise UsageError(_('Not enough arguments supplied')) 609 raise UsageError(_('Not enough arguments supplied'))
607 propname = args[0] 610 propname = args[0]
608 designators = args[1].split(',') 611 designators = args[1].split(',')
609 l = [] 612 linked_props = []
610 for designator in designators: 613 for designator in designators:
611 # decode the node designator 614 # decode the node designator
612 try: 615 try:
613 classname, nodeid = hyperdb.splitDesignator(designator) 616 classname, nodeid = hyperdb.splitDesignator(designator)
614 except hyperdb.DesignatorError as message: 617 except hyperdb.DesignatorError as message:
640 ' Multilink or Link so -d flag does not ' 643 ' Multilink or Link so -d flag does not '
641 'apply.') % propname) 644 'apply.') % propname)
642 propclassname = self.db.getclass(property.classname).classname 645 propclassname = self.db.getclass(property.classname).classname
643 id = cl.get(nodeid, propname) 646 id = cl.get(nodeid, propname)
644 for i in id: 647 for i in id:
645 l.append(propclassname + i) 648 linked_props.append(propclassname + i)
646 else: 649 else:
647 id = cl.get(nodeid, propname) 650 id = cl.get(nodeid, propname)
648 for i in id: 651 for i in id:
649 l.append(i) 652 linked_props.append(i)
650 else: 653 else:
651 if self.print_designator: 654 if self.print_designator:
652 properties = cl.getprops() 655 properties = cl.getprops()
653 property = properties[propname] 656 property = properties[propname]
654 if not (isinstance(property, hyperdb.Multilink) or 657 if not (isinstance(property, hyperdb.Multilink) or
667 '"%(nodeid)s"') % locals()) 670 '"%(nodeid)s"') % locals())
668 except KeyError: 671 except KeyError:
669 raise UsageError(_('no such %(classname)s property ' 672 raise UsageError(_('no such %(classname)s property '
670 '"%(propname)s"') % locals()) 673 '"%(propname)s"') % locals())
671 if self.separator: 674 if self.separator:
672 print(self.separator.join(l)) 675 print(self.separator.join(linked_props))
673 676
674 return 0 677 return 0
675 678
676 def do_set(self, args): 679 def do_set(self, args):
677 ''"""Usage: set items property=value property=value ... 680 ''"""Usage: set items property=value property=value ...
764 # multiple , separated values become a list 767 # multiple , separated values become a list
765 for propname, value in props.items(): 768 for propname, value in props.items():
766 if ',' in value: 769 if ',' in value:
767 values = value.split(',') 770 values = value.split(',')
768 else: 771 else:
769 values = [ value ] 772 values = [value]
770 773
771 props[propname] = [] 774 props[propname] = []
772 # start handling transitive props 775 # start handling transitive props
773 # given filter issue assignedto.roles=Admin 776 # given filter issue assignedto.roles=Admin
774 # start at issue 777 # start at issue
775 curclass = cl 778 curclass = cl
776 lastprop = propname # handle case 'issue assignedto=admin' 779 lastprop = propname # handle case 'issue assignedto=admin'
777 if '.' in propname: 780 if '.' in propname:
778 # start splitting transitive prop into components 781 # start splitting transitive prop into components
779 # we end when we have no more links 782 # we end when we have no more links
780 for pn in propname.split('.'): 783 for pn in propname.split('.'):
781 try: 784 try:
782 lastprop=pn # get current component 785 lastprop = pn # get current component
783 # get classname for this link 786 # get classname for this link
784 try: 787 try:
785 curclassname = curclass.getprops()[pn].classname 788 curclassname = curclass.getprops()[pn].classname
786 except KeyError: 789 except KeyError:
787 raise UsageError(_("Class %(curclassname)s has " 790 raise UsageError(_("Class %(curclassname)s has "
800 803
801 # now do the filter 804 # now do the filter
802 try: 805 try:
803 id = [] 806 id = []
804 designator = [] 807 designator = []
805 props = { "filterspec": props } 808 props = {"filterspec": props}
806 809
807 if self.separator: 810 if self.separator:
808 if self.print_designator: 811 if self.print_designator:
809 id = cl.filter(None, **props) 812 id = cl.filter(None, **props)
810 for i in id: 813 for i in id:
988 991
989 # convert types 992 # convert types
990 for propname in props: 993 for propname in props:
991 try: 994 try:
992 props[propname] = hyperdb.rawToHyperdb(self.db, cl, None, 995 props[propname] = hyperdb.rawToHyperdb(self.db, cl, None,
993 propname, props[propname]) 996 propname, props[propname])
994 except hyperdb.HyperdbValueError as message: 997 except hyperdb.HyperdbValueError as message:
995 raise UsageError(message) 998 raise UsageError(message)
996 999
997 # check for the key property 1000 # check for the key property
998 propname = cl.getkey() 1001 propname = cl.getkey()
999 if propname and propname not in props: 1002 if propname and propname not in props:
1000 raise UsageError(_('you must provide the "%(propname)s" ' 1003 raise UsageError(_('you must provide the "%(propname)s" '
1001 'property.') % locals()) 1004 'property.') % locals())
1002 1005
1003 # do the actual create 1006 # do the actual create
1004 try: 1007 try:
1005 sys.stdout.write(cl.create(**props) + '\n') 1008 sys.stdout.write(cl.create(**props) + '\n')
1006 except (TypeError, IndexError, ValueError) as message: 1009 except (TypeError, IndexError, ValueError) as message:
1120 props = [] 1123 props = []
1121 for spec in prop_names: 1124 for spec in prop_names:
1122 if ':' in spec: 1125 if ':' in spec:
1123 name, width = spec.split(':') 1126 name, width = spec.split(':')
1124 if width == '': 1127 if width == '':
1125 # spec includes trailing :, use label/name width 1128 # spec includes trailing :, use label/name width
1126 props.append((name, len(name))) 1129 props.append((name, len(name)))
1127 else: 1130 else:
1128 try: 1131 try:
1129 props.append((name, int(width))) 1132 props.append((name, int(width)))
1130 except ValueError: 1133 except ValueError:
1144 print(' '.join([name.capitalize().ljust(width) 1147 print(' '.join([name.capitalize().ljust(width)
1145 for name, width in props])) 1148 for name, width in props]))
1146 1149
1147 # and the table data 1150 # and the table data
1148 for nodeid in cl.list(): 1151 for nodeid in cl.list():
1149 l = [] 1152 table_columns = []
1150 for name, width in props: 1153 for name, width in props:
1151 if name != 'id': 1154 if name != 'id':
1152 try: 1155 try:
1153 value = str(cl.get(nodeid, name)) 1156 value = str(cl.get(nodeid, name))
1154 except KeyError: 1157 except KeyError:
1157 # value for it 1160 # value for it
1158 value = '' 1161 value = ''
1159 else: 1162 else:
1160 value = str(nodeid) 1163 value = str(nodeid)
1161 f = '%%-%ds' % width 1164 f = '%%-%ds' % width
1162 l.append(f % value[:width]) 1165 table_columns.append(f % value[:width])
1163 print(' '.join(l)) 1166 print(' '.join(table_columns))
1164 return 0 1167 return 0
1165 1168
1166 def do_history(self, args): 1169 def do_history(self, args):
1167 ''"""Usage: history designator [skipquiet] 1170 ''"""Usage: history designator [skipquiet]
1168 Show the history entries of a designator. 1171 Show the history entries of a designator.
1280 dbclass.restore(nodeid) 1283 dbclass.restore(nodeid)
1281 except KeyError as e: 1284 except KeyError as e:
1282 raise UsageError(e.args[0]) 1285 raise UsageError(e.args[0])
1283 except IndexError: 1286 except IndexError:
1284 raise UsageError(_('no such %(classname)s node ' 1287 raise UsageError(_('no such %(classname)s node '
1285 '" % (nodeid)s"')%locals()) 1288 '" % (nodeid)s"') % locals())
1286 self.db_uncommitted = True 1289 self.db_uncommitted = True
1287 return 0 1290 return 0
1288 1291
1289 def do_export(self, args, export_files=True): 1292 def do_export(self, args, export_files=True):
1290 ''"""Usage: export [[-]class[,class]] export_dir 1293 ''"""Usage: export [[-]class[,class]] export_dir
1307 1310
1308 # get the list of classes to export 1311 # get the list of classes to export
1309 if len(args) == 2: 1312 if len(args) == 2:
1310 if args[0].startswith('-'): 1313 if args[0].startswith('-'):
1311 classes = [c for c in self.db.classes 1314 classes = [c for c in self.db.classes
1312 if c not in args[0][1:].split(',')] 1315 if c not in args[0][1:].split(',')]
1313 else: 1316 else:
1314 classes = args[0].split(',') 1317 classes = args[0].split(',')
1315 else: 1318 else:
1316 classes = self.db.classes 1319 classes = self.db.classes
1317 1320
1329 for classname in classes: 1332 for classname in classes:
1330 cl = self.get_class(classname) 1333 cl = self.get_class(classname)
1331 1334
1332 if not export_files and hasattr(cl, 'export_files'): 1335 if not export_files and hasattr(cl, 'export_files'):
1333 sys.stdout.write('Exporting %s WITHOUT the files\r\n' % 1336 sys.stdout.write('Exporting %s WITHOUT the files\r\n' %
1334 classname) 1337 classname)
1335 1338
1336 with open(os.path.join(dir, classname+'.csv'), 'w') as f: 1339 with open(os.path.join(dir, classname+'.csv'), 'w') as f:
1337 writer = csv.writer(f, colon_separated) 1340 writer = csv.writer(f, colon_separated)
1338 1341
1339 properties = cl.getprops()
1340 propnames = cl.export_propnames() 1342 propnames = cl.export_propnames()
1341 fields = propnames[:] 1343 fields = propnames[:]
1342 fields.append('is retired') 1344 fields.append('is retired')
1343 writer.writerow(fields) 1345 writer.writerow(fields)
1344 1346
1350 # _class.__retired__, _<class>._<keyname> 1352 # _class.__retired__, _<class>._<keyname>
1351 # on imports to rdbms. 1353 # on imports to rdbms.
1352 all_nodes = cl.getnodeids() 1354 all_nodes = cl.getnodeids()
1353 1355
1354 classkey = cl.getkey() 1356 classkey = cl.getkey()
1355 if classkey: # False sorts before True, so negate is_retired 1357 if classkey: # False sorts before True, so negate is_retired
1356 keysort = lambda i: (cl.get(i, classkey), 1358 keysort = lambda i: (cl.get(i, classkey),
1357 not cl.is_retired(i)) 1359 not cl.is_retired(i))
1358 all_nodes.sort(key=keysort) 1360 all_nodes.sort(key=keysort)
1359 # if there is no classkey no need to sort 1361 # if there is no classkey no need to sort
1360 1362
1361 for nodeid in all_nodes: 1363 for nodeid in all_nodes:
1362 if self.verbose: 1364 if self.verbose:
1363 sys.stdout.write('\rExporting %s - %s' % 1365 sys.stdout.write('\rExporting %s - %s' %
1364 (classname, nodeid)) 1366 (classname, nodeid))
1365 sys.stdout.flush() 1367 sys.stdout.flush()
1366 node = cl.getnode(nodeid) 1368 node = cl.getnode(nodeid)
1367 exp = cl.export_list(propnames, nodeid) 1369 exp = cl.export_list(propnames, nodeid)
1368 lensum = sum([len(repr_export(node[p])) for p in propnames]) 1370 lensum = sum([len(repr_export(node[p])) for
1371 p in propnames])
1369 # for a safe upper bound of field length we add 1372 # for a safe upper bound of field length we add
1370 # difference between CSV len and sum of all field lengths 1373 # difference between CSV len and sum of all field lengths
1371 d = sum([len(x) for x in exp]) - lensum 1374 d = sum([len(x) for x in exp]) - lensum
1372 if not d > 0: 1375 if not d > 0:
1373 raise AssertionError("Bad assertion d > 0") 1376 raise AssertionError("Bad assertion d > 0")
1380 cl.export_files(dir, nodeid) 1383 cl.export_files(dir, nodeid)
1381 1384
1382 # export the journals 1385 # export the journals
1383 with open(os.path.join(dir, classname+'-journals.csv'), 'w') as jf: 1386 with open(os.path.join(dir, classname+'-journals.csv'), 'w') as jf:
1384 if self.verbose: 1387 if self.verbose:
1385 sys.stdout.write("\nExporting Journal for %s\n" % classname) 1388 sys.stdout.write("\nExporting Journal for %s\n" %
1389 classname)
1386 sys.stdout.flush() 1390 sys.stdout.flush()
1387 journals = csv.writer(jf, colon_separated) 1391 journals = csv.writer(jf, colon_separated)
1388 for row in cl.export_journals(): 1392 for row in cl.export_journals():
1389 journals.writerow(row) 1393 journals.writerow(row)
1390 if max_len > self.db.config.CSV_FIELD_SIZE: 1394 if max_len > self.db.config.CSV_FIELD_SIZE:
1663 try: 1667 try:
1664 functions = self.commands.get(command) 1668 functions = self.commands.get(command)
1665 except KeyError: 1669 except KeyError:
1666 # not a valid command 1670 # not a valid command
1667 print(_('Unknown command "%(command)s" ("help commands" for a ' 1671 print(_('Unknown command "%(command)s" ("help commands" for a '
1668 'list)') % locals()) 1672 'list)') % locals())
1669 return 1 1673 return 1
1670 1674
1671 # check for multiple matches 1675 # check for multiple matches
1672 if len(functions) > 1: 1676 if len(functions) > 1:
1673 print(_('Multiple commands match "%(command)s": %(list)s') % \ 1677 print(_('Multiple commands match "%(command)s": %(list)s') %
1674 {'command': command, 1678 {'command': command,
1675 'list': ', '.join([i[0] for i in functions])}) 1679 'list': ', '.join([i[0] for i in functions])})
1676 return 1 1680 return 1
1677 command, function = functions[0] 1681 command, function = functions[0]
1678 1682
1706 return 1 1710 return 1
1707 except NoConfigError as message: # noqa: F841 1711 except NoConfigError as message: # noqa: F841
1708 self.tracker_home = '' 1712 self.tracker_home = ''
1709 print(_("Error: Couldn't open tracker: %(message)s") % locals()) 1713 print(_("Error: Couldn't open tracker: %(message)s") % locals())
1710 return 1 1714 return 1
1711 except ParsingOptionError as message: 1715 except ParsingOptionError as message: # message used via locals
1712 print("%(message)s" % locals()) 1716 print("%(message)s" % locals())
1713 return 1 1717 return 1
1714 1718
1715 # only open the database once! 1719 # only open the database once!
1716 if not self.db: 1720 if not self.db:
1775 # handle command-line args 1779 # handle command-line args
1776 self.tracker_home = os.environ.get('TRACKER_HOME', '') 1780 self.tracker_home = os.environ.get('TRACKER_HOME', '')
1777 self.name = 'admin' 1781 self.name = 'admin'
1778 self.password = '' # unused 1782 self.password = '' # unused
1779 if 'ROUNDUP_LOGIN' in os.environ: 1783 if 'ROUNDUP_LOGIN' in os.environ:
1780 l = os.environ['ROUNDUP_LOGIN'].split(':') 1784 login_env = os.environ['ROUNDUP_LOGIN'].split(':')
1781 self.name = l[0] 1785 self.name = login_env[0]
1782 if len(l) > 1: 1786 if len(login_env) > 1:
1783 self.password = l[1] 1787 self.password = login_env[1]
1784 self.separator = None 1788 self.separator = None
1785 self.print_designator = 0 1789 self.print_designator = 0
1786 self.verbose = 0 1790 self.verbose = 0
1787 for opt, arg in opts: 1791 for opt, arg in opts:
1788 if opt == '-h': 1792 if opt == '-h':
1812 return 1 1816 return 1
1813 self.separator = ' ' 1817 self.separator = ' '
1814 elif opt == '-d': 1818 elif opt == '-d':
1815 self.print_designator = 1 1819 self.print_designator = 1
1816 elif opt == '-u': 1820 elif opt == '-u':
1817 l = arg.split(':') 1821 login_opt = arg.split(':')
1818 self.name = l[0] 1822 self.name = login_opt[0]
1819 if len(l) > 1: 1823 if len(login_opt) > 1:
1820 self.password = l[1] 1824 self.password = login_opt[1]
1821 1825
1822 # if no command - go interactive 1826 # if no command - go interactive
1823 # wrap in a try/finally so we always close off the db 1827 # wrap in a try/finally so we always close off the db
1824 ret = 0 1828 ret = 0
1825 try: 1829 try:

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