Ejemplo n.º 1
0
    def run(self, jira, args):
        parser = argparse.ArgumentParser()
        parser.add_argument('dir', nargs='?')
        try:
            args = parser.parse_args(args)
        except:
            return
	project = Project('fP1', 'Fake Project 1', 'ted')
	if not 'fP1' in Jira.cache.data:
	    Jira.cache.data['fP1'] = project
            docid = Jira.cache.document_map.add(
                ['jira', '', '', project.key]
            )
            Jira.cache.catalog.index_doc(docid, project)
	release = Release('f1.0')
	if not 'f1.0' in Jira.cache.data:
	    Jira.cache.data['fP1']['f1.0'] = release
	for id in range(1, 20):
	    data = {}
	    data['key'] = 'fS-%d' % id
	    data['title'] = 'Story %s' % id
	    data['points'] = [1,2,3,5,8,13,21][int(random() * 7)]
	    data['ready'] = datetime(2015, 1, 1)
            started = int(random() * 15 + 1)
	    data['started'] = datetime(2015, 1, started)
            if random() > 0.4:
                month = 1
                resolved = started + int(random() * data['points'] * 2)
                if resolved > 30:
                    month = 2
                    resolved = resolved - 30
	        data['resolved'] = datetime(2015, month, resolved)
	    data['dev'] = ['ann', 'nic', 'sam', 'mia'][int(random() * 4)]
            story = make_story(data)
	    release.add_story(story)
            docid = Jira.cache.document_map.add(
                ['jira', 'fP1', 'f1.0', story.key]
            )
            Jira.cache.catalog.index_doc(docid, story)
        print 'Created fake project "fP1"'
Ejemplo n.º 2
0
class Command(BaseCommand):
    help = 'Render various charts'
    usage = 'chart [team] [-o chart_type] [-d developer] [-s sort_by]' \
        ' [-p point] [-c cycle_time] [-x file_name.ext] [-t issue types] [-f]' \
        ' [-l [issue keys]]'
    options_help = '''    -c : specify cycle time outlier limit
    -d : chart for developer
    -e : include estimates subplot
    -k : chart using or surpressing specific issue keys
    -f : calculate cycle times from the first in process date (default is last)
    -g : group stories by this strategy (default || estimate || resolved)
    -l : label points
    -o : specify chart type: cycle (default) || hist || arrival [state]
    -p : priority to chart
    -s : sorting criteria
    -t : issue types to chart
    -v : chart for estimate value 
    -x : export graph to a file (valid extensions are pdf, png, or jpg)
    '''
    examples = '''    chart
    chart App
    chart -k !1234
    chart -o arrival 10090
    chart -s scrum_team points -t 3 7 15'''

    def run(self, jira, args):
        parser = argparse.ArgumentParser()
        parser.add_argument('team', nargs='?')
        parser.add_argument('-c', nargs='*', required=False)
        parser.add_argument('-d', nargs='*', required=False)
        parser.add_argument('-e', action='store_true', required=False)
        parser.add_argument('-f', action='store_true', required=False)
        parser.add_argument('-g', nargs='?', required=False)
        parser.add_argument('-l', nargs='*', required=False)
        parser.add_argument('-k', nargs='*', required=False)
        parser.add_argument('-o', nargs='*', required=False)
        parser.add_argument('-p', nargs='*', required=False)
        parser.add_argument('-v', nargs='*', required=False)
        parser.add_argument('-s', nargs='*', required=False)
        parser.add_argument('-t', nargs='*', required=False)
        parser.add_argument('-x', nargs='*', required=False)
        try:
            self.args = parser.parse_args(args)
        except:
            return
        self.release = jira.cache.get_by_path(jira.cache.cwd)
        if not isinstance(self.release, Release):
            print 'Error: Must navigate to a release. (hint: help cd)'
            return
        if self.args.f:
            self.cycle_time = 'aggregate_cycle_time'
        else:
            self.cycle_time = 'cycle_time'
        types = ['7']
        if self.args.t:
            if len(self.args.t) == 1:
                types = self.args.t[0].split(',')
            else:
                types = self.args.t
        if self.args.team:
            stories = [s for s in self.release.clean_stories(type=types)
                if s.scrum_team and s.scrum_team[:len(self.args.team)] \
                    == self.args.team]
            self.release = Release()
            for story in stories:
                self.release.add_story(story)
        if self.args.d:
            stories = [s for s in self.release.clean_stories(type=types)
                if s.developer and s.developer[:len(self.args.d[0])] \
                    == self.args.d[0]]
            self.release = Release()
            for story in stories:
                self.release.add_story(story)
        if self.args.v:
            stories = [s for s in self.release.clean_stories(type=types)
                if s.points and s.points == float(self.args.p[0])]
            self.release = Release()
            for story in stories:
                self.release.add_story(story)
        if self.args.c:
            cycle_time = int(self.args.c[0])
            stories = [s for s in self.release.clean_stories(type=types)
                if getattr(s, self.cycle_time) < cycle_time]
            self.release = Release()
            for story in stories:
                self.release.add_story(story)
        if self.args.k:
            hide_keys = []
            show_keys = []
            for k in self.args.k:
                if k[0] == '!':
                    hide_keys.append('NG-' + k[1:])
                else:
                    show_keys.append('NG-' + k)
            if show_keys:
                stories = [s for s in self.release.clean_stories(type=types)
                    and s.key in show_keys]
                self.release = Release()
                for story in stories:
                    self.release.add_story(story)
            if hide_keys:
                stories = [s for s in self.release.clean_stories(type=types)
                    and s.key not in hide_keys]
                self.release = Release()
                for story in stories:
                    self.release.add_story(story)
        if self.args.x:
            self.file = 'cycles-%s.%s' % (self.release.version, self.args.x[0])
        else:
            self.file = None
        kanban = self.release.kanban()
        stories = self.release.clean_stories(type=types)
        if self.args.p:
            stories = [s for s in stories if s.priority in self.args.p]
        if not stories:
            print 'No data to report'
            return
        stories.sort(key=lambda i:i.key)
        if self.args.s:
            sorting = self.args.s
            if 'cycle_time' not in sorting:
                sorting.append('cycle_time')
        else:
            sorting = ['cycle_time']

        if self.args.o and self.args.o[0] == 'hist':
            self.histogram(stories)
        elif self.args.o and self.args.o[0] == 'arrival':
            if len(self.args.o) == 2:
                if self.args.o[1] in self.release.WIP.keys():
                    state = self.release.WIP[self.args.o[1]]
                elif int(self.args.o[1]) in self.release.WIP.values():
                    state = int(self.args.o[1])
                else:
                    print 'Invalid state specified: %s' % self.args.o[1]
                    return
                self.arrivals(stories, state)
            else:
                self.arrivals(stories)
        elif not self.args.o or self.args.o[0] == 'cycles':
            self.cycles(stories, sorting)
        else:
            print 'Unknown chart type: %s' % self.args.t[0]

    def histogram(self, stories):
        bins = len(stories)/5
        if bins < 10:
            bins = 10
        cycle_times = [s.cycle_time for s in stories if s.cycle_time]
        param = scipy.stats.norm.fit(cycle_times)
        x = numpy.linspace(min(cycle_times), max(cycle_times), 200)
        pdf_fitted = scipy.stats.norm.pdf(x, loc=param[0], scale=param[1])
        pylab.plot(x, pdf_fitted, 'r-', label='Fitted')
        pylab.hist(cycle_times, bins, normed=True, alpha=.3)
        pylab.show(block=False)

    def arrivals(self, stories, state=6):
        ''' Chart a plot point for every arrival time in state
        '''
        arrivals = self.release.kanban().state_arrival_interval(state)
        dates = [a['date'] for a in arrivals]
        arrivals = [round(a['interval']/60./60., 1) for a in arrivals]
        average = numpy.median([arrivals])
        std = numpy.std([arrivals])
        iql = numpy.percentile([arrivals], 25)
        iqh = numpy.percentile([arrivals], 75)
        nsul = []
        nsuw = []
        nsll = []
        nslw = []
        avg = []
        for x in arrivals:
            nsul.append(average + (iqh * 3))
            nsuw.append(average + (iqh * 2))
            nslw.append(average - (iql * 2))
            nsll.append(average - (iql * 3))
            avg.append(average)
        pyplot.plot(dates, arrivals, '*', color='g')
        pyplot.plot(dates, nsul, 'o', linestyle='-', color='r')
        pyplot.plot(dates, nsuw, '.', linestyle=':', color='y')
        pyplot.plot(dates, nslw, '.', linestyle=':', color='y')
        pyplot.plot(dates, nsll, 'o', linestyle='-', color='r')
        pyplot.plot(dates, avg, '',linestyle='-.',  markerfacecolor='None')
        pyplot.show(block=False)

    def cycles(self, stories, sorting):
        data = []
        wip = []
        estimates = []
        estimate_labels = []
        developer_labels = []
        alldata = []
        labels = []
        ratios = []
        count = [0]
        def compare(a, b):
            if not a[0]:
                return -1
            if not b[0]:
                return 1
            return cmp(a, b)
        stories.sort(key=lambda x:tuple([getattr(x, key) for key in sorting]),
            cmp=compare)
        for story in stories:
            if not story.started:
                continue
            alldata.append(getattr(story, self.cycle_time))
            if not story.resolved:
                wip.append(getattr(story, self.cycle_time))
                data.append(None)
            else:
                data.append(getattr(story, self.cycle_time))
                wip.append(None)
            estimates.append(story.points)
            labels.append(story.key)
            estimate_labels.append(story.key)
            developer_labels.append(story.developer)
            if self.args.g and not self.args.g == 'default':
                if self.args.g == 'estimate':
                    count.append(getattr(story, sorting[0]))
                elif self.args.g == 'resolved':
                    if story.resolved:
                        count.append(story.resolved)
                    elif story.started:
                        count.append(story.started)
                    else:
                        count.append(datetime.datetime.now())
            else:
                count.append(count[-1] + 1)

        all_non_empty_data = [d for d in alldata if d]
        if not all_non_empty_data:
            print 'Nothing to do. Probably need to finish some work.'
            return
        std = numpy.std(all_non_empty_data)
        iql = numpy.percentile(all_non_empty_data, 25)
        iqh = numpy.percentile(all_non_empty_data, 75)
        average = numpy.median(all_non_empty_data)
        nsul = []
        nsuw = []
        nsll = []
        nslw = []
        avg = []
        for x in data:
            nsul.append(average + (iqh * 3))
            nsuw.append(average + (iqh * 2))
            nslw.append(average - (iql * 2))
            nsll.append(average - (iql * 3))
            avg.append(average)
        if self.args.e:
            gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1]) 
            pyplot.subplot(gs[0])
        pyplot.plot(count[1:], data, '*', color='g')
        pyplot.plot(count[1:], wip, '^', color='r')
        pyplot.plot(count[1:], nsul, 'o', linestyle='-', color='r')
        pyplot.plot(count[1:], nsuw, '.', linestyle=':', color='y')
        pyplot.plot(count[1:], nslw, '.', linestyle=':', color='y')
        pyplot.plot(count[1:], nsll, 'o', linestyle='-', color='r')
        pyplot.plot(count[1:], avg, '',linestyle='-.',  markerfacecolor='None')

        previous_y = None 
        y_label = 10
        for label, x, y in zip(labels, count[1:], alldata):
            if not y:
                y = 0.0
            if y == previous_y:
                y_label += 10
            else:
                previous_y = y
                y_label = 10
            if self.args.l is None:
                if y < iqh * 3 + average:
                    continue
            if self.args.l is not None and len(self.args.l) \
                and label not in self.args.l:
                continue
            pyplot.annotate(
            label,
            url='http://www.google.com',
            xy=(x, y), xytext=(-10,y_label),
            textcoords = 'offset points', ha='right', va='bottom', fontsize=7,
            bbox = dict(boxstyle = 'round,pad=0.3', fc='yellow', alpha=0.5),
                arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0'))
        yoffset = -10
        odd = True
        for label, x, y in zip(developer_labels, count[1:], nsll):
            if not self.args.l:
                continue
            if odd:
                odd = False
            else:
                odd = True
                continue
            if not label:
                continue
            if yoffset < -30:
                yoffset = -10
            pyplot.annotate(
            label,
            url='http://www.google.com',
            xy=(x, y), xytext=(10, yoffset),
            textcoords = 'offset points', ha='right', va='bottom', fontsize=8,
            bbox = dict(boxstyle = 'round,pad=0.3', fc='cyan', alpha=0.1),
                arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0'))
            yoffset -= 10
        odd = True
        yoffset = 10
        for label, x, y in zip(developer_labels, count[1:], nsul):
            if not self.args.l:
                continue
            if not self.args.l:
                if y < iqh * 3:
                    continue
            if odd:
                odd = False
                continue
            else:
                odd = True
            if not label:
                continue
            if yoffset > 30:
                yoffset = 10
            pyplot.annotate(
            label,
            url='http://www.google.com',
            xy=(x, y), xytext=(10, yoffset),
            textcoords = 'offset points', ha='right', va='bottom', fontsize=8,
            bbox = dict(boxstyle = 'round,pad=0.3', fc='cyan', alpha=0.1),
                arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0'))
            yoffset += 10

        if not self.args.e:
            if self.file:
                pyplot.savefig(self.file, bbox=0)
            else:
                pyplot.show(block=False)
            return

        pyplot.grid(True)
        pyplot.subplot(gs[1])
        pyplot.plot(count[1:], estimates, 'o', linestyle='', color='b')
        previous_label = ''
        label_count = 0
        elevated = True
        for label, x, y in zip(estimate_labels, count[1:], estimates):
            if not self.args.l:
                continue
            if label == previous_label:
                label_count += 1
                continue
            if label_count <=1 and not elevated:
                elevated = True
                yoffset = 25
            else:
                elevated = False
                yoffset = 10
            label_count = 0
            previous_label = label
            pyplot.annotate(
            label,
            url='http://www.google.com',
            xy=(x, y), xytext=(-10,yoffset),
            textcoords = 'offset points', ha='right', va='bottom', fontsize=7,
            bbox = dict(boxstyle = 'round,pad=0.3', fc='yellow', alpha=0.5),
                arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0'))

        if self.file:
            pyplot.savefig(self.file, bbox=0)
        else:
            pyplot.show(block=False)
Ejemplo n.º 3
0
class Command(BaseCommand):
    help = 'Report on the current release'
    usage = 'report [team] [-d developer]'
    options_help = '''    -d : report on a specific developer'''
    exmamples = '''    report
    report App'''

    def run(self, jira, args):
        parser = argparse.ArgumentParser()
        parser.add_argument('team', nargs='?')
        parser.add_argument('-d', nargs='*', required=False)
        parser.add_argument('-f', action='store_true', required=False)
        try:
            args = parser.parse_args(args)
        except:
            return
        self.release = jira.cache.get_by_path(jira.cache.cwd)
        if not isinstance(self.release, Release):
            print 'Error: Must navigate to a release. (hint: help cd)'
            return
        if args.team:
            stories = [s for s in self.release.values()
                if s.scrum_team and s.scrum_team[:len(args.team)] == args.team]
            self.release = Release()
            for story in stories:
                self.release.add_story(story)
        if args.d:
            stories = [s for s in self.release.values()
                if s.developer and s.developer[:len(args.d[0])] == args.d[0]]
            self.release = Release()
            for story in stories:
                self.release.add_story(story)
        if not self.release.keys():
            print 'No data to report'
            return
        release = self.release
        kanban = release.kanban()
        kanban.average_cycle_times_by_type()
        smallest = 'None'
        if release.sort_by_size():
            smallest = release.sort_by_size()[-1].points
        largest = 'None'
        if release.sort_by_size():
            largest = release.sort_by_size()[0].points
        print 'Points in scope  :', round(release.total_points(), 1)
        print 'Points completed :', round(release.points_completed(), 1)
        print 'Total WIP        :', release.wip()
        print 'Stories          :', release.total_stories()
        print '  Avg Points     :', round(release.average_story_size(), 1)
        print '  Std Dev Points :', round(release.std_story_size(), 1)
        print '  Smallest points:', smallest
        print '  Largest points :', largest 
        print '  Story WIP      :', release.stories_in_process()
        print '  Avg Cycle Time :', kanban.average_cycle_time()
        print '  Std Cycle Time :', kanban.stdev_cycle_time()
        print '  Skew           :', release.skew_cycle_time()
        print '  m Cycle Time   :', kanban.median_cycle_time()
        print '  Total Variance :', kanban.variance_cycle_time()
        print '  Total Dev CT   :', release.aggregate_developer_cycle_time()
        print '  Avg Dev CT     :', release.average_developer_cycle_time()
        print '  Std Dev CT     :', release.stdev_developer_cycle_time()
        print 'Bugs             :', len(release.bugs())
        print '  Production     :', len(release.stories(type=['78']))
        print '    Closed         :', len(release.resolved_stories(['78']))
        print '    Avg Cycle Time :', kanban.average_lead_time(type=['78'])
        print '    m Cycle Time   :', kanban.median_lead_time(type=['78'])
        print '    Std Cycle Time :', kanban.stdev_lead_time(type=['78'])
        print '  Development    :', len(release.stories(type=['1']))
        print '    Closed         :', len(release.resolved_stories(['1']))
        print '    Avg Cycle Time :', kanban.average_lead_time(type=['1'])
        print '    m Cycle Time   :', kanban.median_lead_time(type=['1'])
        print '    Std Cycle Time :', kanban.stdev_lead_time(type=['1'])
        print
        print 'WIP by Status:'
        wip = release.wip_by_status()
        print 'Status:', '           WIP:'
        for key in wip:
            print humanize(int(key)).ljust(16), ':', \
                str(wip[key]['wip']).ljust(6)
        print
        print 'Total Cycle Times by Status (days):'
        cycle_times_in_status = kanban.cycle_times_in_status()
        total = 0
        cycle_times = []
        for status in cycle_times_in_status.keys():
            if status in (1, 10089, 6): # ignore 'open' and 'ready'
                continue
            total += cycle_times_in_status[status]
            cycle_times.append((str(status), cycle_times_in_status[status]))
        cycle_times.sort(key=lambda x:x[1], reverse=True)
        for cycle_time in cycle_times:
            print humanize(int(cycle_time[0])).ljust(5), ':', \
                str(cycle_time[1]).ljust(4), \
                '%' + str(round(cycle_time[1]/float(total), 2) * 100)
        print 'Total   :', total
        print 'Max PCE : %' + str(kanban.process_cycle_efficiency())
        print
        print 'Average Takt Time in Status (days):'
        print '  Status:', 'Average:', 'Stdev:'
        averages = kanban.average_times_in_status()
        stds = kanban.std_times_in_status()
        for key in averages:
            print ' ', humanize(key).ljust(7), str(averages[key]).ljust(8), stds[key]
        print
        print 'Arrival Times for Status (days):'
        print '  Status:', 'Average:', 'Stdev:'
        averages = kanban.average_arrival_for_status()
        stds = kanban.std_arrival_for_status()
        for key in averages:
            print ' ', humanize(key).ljust(7), str(averages[key]).ljust(8), stds[key]
        print
        print 'State Transition Probabilities:'
        states = kanban.state_transition_probabilities()
        print 'Status: ', 'Stories:', 'Days:', 'Avg:', 'Std:'
        for state in states:
            print humanize(state) + ':'
            for exit in states[state]:
                print '  ', humanize(exit).ljust(5), str(len(states[state][exit]['days'])).ljust(8), str(sum(states[state][exit]['days'])).ljust(5), str(states[state][exit]['average']).ljust(5), states[state][exit]['std']

        intervals = kanban.all_state_arrival_intervals()
        print
        print 'Arrival Intervals:'
        for state in intervals:
            print humanize(state).ljust(5), \
                  str(intervals[state]['average']).ljust(7), \
                  intervals[state]['std']

        bugs = kanban.average_cycle_times_by_bug_count(type=['7'])
        print
        print 'Cycle Times By Bug Count:'
        print 'Bugs:', 'CT:'
        for bug in bugs:
            print str(bug).ljust(6), bugs[bug]

        print
        print 'Root Causes:'
        causes = {}
        for story in release.bugs():
            if story.root_cause not in causes:
                causes[story.root_cause] = [story.key]
            else:
                causes[story.root_cause].append(story.key)