aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Gall <tom.gall@linaro.org>2015-10-14 14:34:28 -0500
committerTom Gall <tom.gall@linaro.org>2015-10-14 14:34:28 -0500
commite69cf64872fe299512467abf9b0579c70e101481 (patch)
tree18360122b32823717d78bed6c71240c3736484b3
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-xinitiative-report.py349
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()