diff options
author | Tom Gall <tom.gall@linaro.org> | 2015-10-14 14:34:28 -0500 |
---|---|---|
committer | Tom Gall <tom.gall@linaro.org> | 2015-10-14 14:34:28 -0500 |
commit | e69cf64872fe299512467abf9b0579c70e101481 (patch) | |
tree | 18360122b32823717d78bed6c71240c3736484b3 |
Initial beta of initiative-report a python tool to create an ascii
command line report by initiative of activities, listing out both
the structure with status and by engineer working on the initiative.
-rwxr-xr-x | initiative-report.py | 349 |
1 files changed, 349 insertions, 0 deletions
diff --git a/initiative-report.py b/initiative-report.py new file mode 100755 index 0000000..2f6b568 --- /dev/null +++ b/initiative-report.py @@ -0,0 +1,349 @@ +#!/usr/bin/env python +# Copyright (C) 2015 Linaro, Tom Gall +# +# Author: Tom Gall <tom.gall@linaro.org> +# +# Convert from the old cards system to projects. +# Effortlessly. ( I hope ) +# +# This is distributed WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with cards2projects.py. If not, see <http://www.gnu.org/licenses/>. +# + +import ConfigParser +import argparse +import logging +import sys +import datetime +import codecs +import locale +import re +import urllib3.contrib.pyopenssl +urllib3.contrib.pyopenssl.inject_into_urllib3() + +from jira.client import JIRA + +DEFAULT_LOGGER_NAME = "test.log" +logger = None +__version__ = "2015.10" + +def connect_projects(logger): + """Connect to projects Instance + + """ + Config = ConfigParser.ConfigParser() + Config.read("settings.cfg") + projects_server = "https://projects.linaro.org/" + jira_user = Config.get('Jira', 'Username') + jira_pass = Config.get('Jira', 'Password') + + try: + logger.info("Connection to %s" % projects_server) + jira = JIRA(options={'server': projects_server}, basic_auth=(jira_user, jira_pass)) + return jira + except: + logger.error("Failed to connect to projects.linaro.org") + return None + + +def get_logger(args, name=DEFAULT_LOGGER_NAME ): + """ + Retrieves a named logger. Default name is set in the variable + DEFAULT_LOG_NAME. Debug is set to False by default. + + :param name: The name of the logger. + :param debug: If debug level should be turned on + :return: A logger instance. + """ + logger = logging.getLogger(name) + ch = logging.StreamHandler() + + if args.debug: + ch.setLevel(logging.DEBUG) + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s") + ch.setFormatter(formatter) + logger.setLevel(logging.DEBUG) + else: + ch.setLevel(logging.INFO) + formatter = logging.Formatter("%(message)s") + ch.setFormatter(formatter) + logger.setLevel(logging.INFO) + + logger.addHandler(ch) + return logger + + +def setup_args_parser(): + """ + Setup the argument parsing. + + :return: The parsed arguments. + """ + description = "Walk through the issue under an Initiative and generate some metrics" + parser = argparse.ArgumentParser(description=description) + parser.add_argument("-d", "--debug", action="store_true") + # parser.add_argument("-c", "--component", required=True, help="cards.l.o Component") + parser.add_argument("-p", "--project", required=True, help="projects.l.o Project") + #parser.add_argument("--only_epics", action="store_true", help="List only epics, default is epcis and cards") + #parser.add_argument("-e", "--epic", help="Transfer just this epic and children") + parser.add_argument("-i", "--initiative", required=True, help="Transfer just this card and children") + #parser.add_argument("--commit", action="store_true", help="Actually do migration and commit results") + + return parser.parse_args() + + +def get_issue(jira, issue): + """ + get_card - get a card and all fields + + :return: card with expanded comments + """ + #print "get card" + issue = jira.issue(issue.key) + #print 'found: ' + issue.key + # print 'description: ' + issue.fields.description.encode('ascii','ignore') + '\n' + + return issue + + +def constructinitiativequery(args): + basequery = ' project = LMG AND issuekey = ' + args.initiative + + #if args.stale==True: + # basequery += ' AND updated < -14d ' + # basequery += ' AND status != Closed' + + basequery += ' ORDER BY rank' + return basequery + + +def constructstoryquery(args): + basequery = ' project = LMG AND issuetype = story ' + + #if args.stale==True: + # basequery += ' AND updated < -14d ' + # basequery += ' AND status != Closed' + + basequery += ' ORDER BY rank' + return basequery + + +def walkcards(): + Config = ConfigParser.ConfigParser() + Config.read("settings.cfg") + + args = setup_args_parser() + + global logger + logger = get_logger(args) + + projects = connect_projects(logger) + + if projects is None: + sys.exit(1) + + basequery = constructinitiativequery(args) + i = projects.search_issues( basequery) + for initiative in i: + print "Initiative " + initiative.key + initiative = get_issue(projects, initiative) + + topinitiative = {} + topinitiative['issue'] = initiative + topinitiative['links'] = [] + + Stats = { 'numberEpics' : 0, 'numberStories':0, 'numberSubtasks' : 0 } + + basequery = constructstoryquery(args) + allstories = projects.search_issues( basequery, maxResults=500) + + orphanengineerlist = { } + engineernode = { 'name' : 'None', 'storylist' : [ ], 'subtasklist' : [ ], 'storyinInit' : [], 'subtaskinInit' : [] } + orphanengineerlist['None'] = engineernode + + orphanstorieslist = [] + + """ + issue states + 1 = open + 811 = todo + 911 = In Progress? + 951 = blocked + 921 = upstreaming review + 931 = review + 941 = resolved + 791 = closed (I think) + """ + + # for all stories make node objects and engineer objects + number = len(allstories) + print "number stories found is : ", number + for astory in allstories: + astory = get_issue(projects, astory) + # done is a count, complete is a state + storynode = { 'issue': astory, 'links' : [], 'done' : 0, 'total' : 0, 'active' : 0, 'complete':0 } + orphanstorieslist.append(storynode) + engineernode = { } + # print "astory is " + astory.key + 'state' + astory.fields.status.id + if astory.fields.assignee is not None: + if astory.fields.assignee.displayName in orphanengineerlist: + engineernode = orphanengineerlist[astory.fields.assignee.displayName] + else: + engineernode = { 'name' : '', 'storylist' : [ ], 'subtasklist' : [ ], 'storyinInit' : [], 'subtaskinInit' : [] } + engineernode['name'] = astory.fields.assignee.displayName + orphanengineerlist[astory.fields.assignee.displayName] = engineernode + engineernode['storylist'].append(storynode) + else: + engineernode = orphanengineerlist['None'] + engineernode['storylist'].append(storynode) + + if astory.fields.status.id in ['3']: + storynode['active'] = 1 + elif astory.fields.status.id in ['5']: + storynode['complete'] = 1 + elif astory.fields.status.id in ['6']: + storynode['complete'] = 1 + + #print 'Story : ' + astory.key + for subtasklink in astory.fields.subtasks: + try: + subtasknode = {'issue' : None, 'links' : [], 'active': 0, 'complete': 0 } + if subtasklink.key is not None: + subtask = projects.issue(subtasklink.key) + subtasknode['issue'] = subtask + # print ' Subtask : ' + subtasklink.key + ' status ' + subtask.fields.status.id + if subtask.fields.assignee is not None: + if subtask.fields.assignee.displayName in orphanengineerlist: + engineernode = orphanengineerlist[subtask.fields.assignee.displayName] + else: + engineernode = { 'name' : '', 'storylist' : [ ], 'subtasklist' : [ ], 'storyinInit' : [], 'subtaskinInit' : [] } + engineernode['name'] = subtask.fields.assignee.displayName + orphanengineerlist[subtask.fields.assignee.displayName] = engineernode + engineernode['subtasklist'].append(subtasknode) + else: + engineernode = orphanengineerlist['None'] + engineernode['storylist'].append(storynode) + + if subtask.fields.status.id in ['1']: + #print 'match 1 ',+ storynode['total'], ' story: ' + astory.key + ' subtask: ' + subtask.key + storynode['total'] = storynode['total'] + 1 + #print 'after total ', storynode['total'] + elif subtask.fields.status.id in ['3']: + storynode['total'] = storynode['total'] + 1 + subtasknode['active'] = 1 + elif subtask.fields.status.id in ['4']: + storynode['total'] = storynode['total'] + 1 + elif subtask.fields.status.id in ['5']: + storynode['done'] = storynode['done'] + 1 + storynode['total'] = storynode['total'] + 1 + subtasknode['complete'] = 1 + elif subtask.fields.status.id in ['6']: + storynode['done'] = storynode['done'] + 1 + storynode['total'] = storynode['total'] + 1 + subtasknode['complete'] = 1 + + storynode['links'].append(subtasknode) + except Exception as e: + #print "next subtask" + print str(e) + pass + + # traverse EPICs, stories and subtasks + print 'Number epic issuelinks : ' + str(len(initiative.fields.issuelinks)) + for epiclink in initiative.fields.issuelinks: + try: + if epiclink.inwardIssue is not None: + epic = get_issue(projects, epiclink.inwardIssue) + # print "Epic " + epic.key + + epicnode = { 'issue': epic, 'links' : [] } + + topinitiative['links'].append(epicnode) + #print "epic " + epic.key + #print len(topinitiative['links']) + # now look for any stories owned by this EPIC and connect them + for astory in orphanstorieslist: + try: + story = astory['issue'] + #print 'Story examined : ' + story.key + epicconnection = story.fields.customfield_10005 + if epicconnection is not None: + if epicconnection in epic.key: + #print 'match found ' + epicconnection + ' ' + epic.key + # orphanstorieslist.remove(astory) + epicnode['links'].append(astory) + engineernode = orphanengineerlist[story.fields.assignee.displayName] + engineernode['storyinInit'].append(astory) + # walk subtasks for engineer stats + for asubtask in astory['links']: + engineernode['subtaskinInit'].append(asubtask) + + Stats['numberSubtasks'] = Stats['numberSubtasks'] + len(astory['links']) + Stats['numberStories'] = Stats['numberStories'] + 1 + except Exception as e: + #print "next story" + print str(e) + pass + except Exception as e: + print "next epiclink" + print str(e) + pass + + # compile stats + Stats['numberEpics'] = len(topinitiative['links']) + # generate report + print "Inititiave "+ initiative.key + print "Summary:" + print " EPICs: " + str( Stats['numberEpics']) + print " Stories:" , Stats['numberStories'] + print " Subtasks:", Stats['numberSubtasks'] + print "By Epic:" + for e in topinitiative['links']: + issuenode = e['issue'] + print " Epic: " + issuenode.key + ' ' + issuenode.fields.summary + for s in e['links']: + storynode = s['issue'] + if s['active'] == 1: + state = 'A ' + elif s['complete'] == 1: + state = 'D ' + else: + state = ' ' + print " Story: " + state + "(",s['done'] ,"/", s['total'] ,") " + storynode.key + ' ' + storynode.fields.summary + #print someValue, someValue 'or ' (someValue/someValue) + " xx% complete" + print "By Engineer:" + for name, node in orphanengineerlist.items(): + if len(node['storyinInit'])!=0 or len(node['subtaskinInit'])!=0: + print "Name: " + name.encode('utf-8').decode('ascii', 'ignore') + if len(node['storyinInit'])!=0: + print "Stories: ", len(node['storyinInit']) + for s in node['storyinInit']: + storynode = s['issue'] + if s['active'] == 1: + state = 'A ' + elif s['complete'] == 1: + state = 'D ' + else: + state = ' ' + print " Story: "+state+"(",s['done'] ,"/", s['total'] ,") " + storynode.key + ' ' + storynode.fields.summary + if len(node['subtaskinInit'])!=0: + print "Subtasks: ", len(node['subtaskinInit']) + for s in node['subtaskinInit']: + subtasknode = s['issue'] + if s['active'] == 1: + state = 'A ' + elif s['complete'] == 1: + state = 'D ' + else: + state = ' ' + print " Subtask: "+state + subtasknode.key + ' ' + subtasknode.fields.summary + + +if __name__ == '__main__': + sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout) + walkcards() |