def test_get_end_date(self, mock_config): mock_config.get_config_value = mock.MagicMock( return_value='2017-01-22') # App init, necessary to get to the logging service app = self.get_app() time = Time(app.log, mock_config) end_date_answer = datetime.strptime('2017-01-22', "%Y-%m-%d").date() end_date_response = time.get_end_date() self.assertEqual(end_date_answer, end_date_response)
class Load(object): """ Load updated Jira data into cache """ def __init__(self, log, app_config): self.log = log self.config = Config(self.log) self.time = Time(self.log, self.config) self.log_config = LogConfig(self.log, app_config, self.config.config_path + 'load.log') def refresh_jira_cache(self): self.log.info('Load updated data from Jira into Cache') date_start = self.time.get_current_date() date_end = self.time.get_end_date() self.log.info('Load.main(): Start Date: ' + date_start.strftime('%Y-%m-%d')) self.log.info('Load.main(): End Date: ' + date_end.strftime('%Y-%m-%d')) loader = ImportData(self.log, self.config) # Import existing data (if any) into a Python object previous_data = Files(self.log).jsonl_load( self.config.filepath_data_completion) # Refresh the cache by checking if additional days can be added daily_data = loader.refresh_dailydata_cache(previous_data, date_start, date_end) # Write back the data cache to file after clearing any existing one loader.write_dailydata_cache(daily_data) # Call Jira to get Remaining work remaining_work = loader.get_remaining_work() return daily_data, remaining_work def load_jira_cache(self): self.log.info('Load daily data and remaining work from cache') # Import existing data (if any) into a Python object daily_data = Files(self.log).jsonl_load( self.config.filepath_data_completion) remaining_work = Files(self.log).json_load( self.config.filepath_data_remaining) if remaining_work is None or daily_data is None: self.log.error( 'Unable to load cached data, please run \'jav load\' first') exit() return daily_data, remaining_work
def __init__(self, log, config): self.log = log self.config = config self.time = Time(self.log, self.config)
class BuildChart(object): def __init__(self, log, config): self.log = log self.config = config self.time = Time(self.log, self.config) def build_url_completed(self, current_date): return \ self.config.get_config_value('jira_host') + '/issues/?jql=' \ + quote( self.config.get_config_value('jira_jql_velocity') + ' ON(\"' + current_date.strftime("%Y-%m-%d") + '\")' ) def build_url_remaining(self, jira_field, current_type): return \ self.config.get_config_value('jira_host') + '/issues/?jql=' \ + quote( self.config.get_config_value('jira_jql_remaining') + ' AND ' + jira_field + ' = \"' + current_type + '\"' ) def build_velocity_days(self, stats_data): self.log.info('Generating graph about daily effort') # Velocity Days x_dates = [] y_values = [] y_avg = [] x_dates_display = [] jira_url = [] for scan_day in stats_data: x_dates.append(stats_data[scan_day]['datetime']) x_dates_display.append(stats_data[scan_day]['datetime'].strftime('%a %b %d, %Y')) y_values.append(stats_data[scan_day][self.config.get_config_value('stats_metric')]) jira_url.append(self.build_url_completed(stats_data[scan_day]['datetime'])) week_idx = '4' if week_idx not in stats_data[scan_day]['anyday']: week_idx = 4 if 'avg' in stats_data[scan_day]['anyday'][week_idx]: y_avg.append(stats_data[scan_day]['anyday'][week_idx]['avg']) else: y_avg.append(0) # prepare some data data_dates = np.array(x_dates, dtype=np.datetime64) source = ColumnDataSource( data=dict( x_data_dates=data_dates, y_values=y_values, y_avg_values=y_avg, x_dates_display=x_dates_display, jira_url=jira_url ) ) # Declare tools hover = HoverTool( tooltips=[ ('Date', '@x_dates_display'), (self.config.get_config_value('stats_metric').capitalize(), '@y_values'), ('Average', '@y_avg_values'), ] ) # create a new plot with a a datetime axis type p = figure(width=1000, height=350, x_axis_type='datetime', tools=['save', 'pan', 'box_zoom', 'reset', hover, 'tap']) if self.config.get_config_value('stats_metric') == 'tickets': metric_legend = 'Tickets' else: metric_legend = 'Story Points' # add renderers p.circle('x_data_dates', 'y_values', size=4, color='red', alpha=0.4, legend=metric_legend, source=source) p.line('x_data_dates', 'y_avg_values', color='blue', legend='4 Weeks Average', source=source) taptool = p.select(type=TapTool) taptool.callback = OpenURL(url='@jira_url') # NEW: customize by setting attributes p.title.text = 'Daily Velocity' p.legend.location = 'top_left' p.grid.grid_line_alpha = 0 p.xaxis.axis_label = 'Days (Monday-Friday)' p.yaxis.axis_label = metric_legend p.ygrid.band_fill_color = 'olive' p.ygrid.band_fill_alpha = 0.1 return p def build_velocity_weeks(self, stats_data): self.log.info('Generating graph about weekly effort') # Velocity Days x_dates = [] y_values = [] y_avg = [] y_weeks = [] daily_avg = [] for scan_day in stats_data: x_dates.append(stats_data[scan_day]['datetime']) y_values.append(stats_data[scan_day][self.config.get_config_value('stats_metric')]) y_weeks.append(stats_data[scan_day]['weektxt']) daily_avg.append(round(float(stats_data[scan_day][self.config.get_config_value('stats_metric')]) / 5, 1)) if len(stats_data[scan_day]['stats']) > 0: week_idx = '4' if week_idx not in stats_data[scan_day]['stats']: week_idx = 4 if 'avg' in stats_data[scan_day]['stats'][week_idx]: y_avg.append(stats_data[scan_day]['stats'][week_idx]['avg']) else: y_avg.append(0) # prepare some data data_dates = np.array(x_dates, dtype=np.datetime64) source = ColumnDataSource( data=dict( x_data_dates=data_dates, y_values=y_values, y_avg_values=y_avg, y_weeks=y_weeks, daily_avg=daily_avg, ) ) # Declare tools hover = HoverTool( tooltips=[ ('Week', '@y_weeks'), (self.config.get_config_value('stats_metric').capitalize(), '@y_values'), ('Avg ' + self.config.get_config_value('stats_metric') + ' per week', '@y_avg_values'), ('Avg ' + self.config.get_config_value('stats_metric') + ' per day', '@daily_avg'), ] ) # create a new plot with a a datetime axis type p = figure(width=1000, height=350, x_axis_type='datetime', tools=['save', 'pan', 'box_zoom', 'reset', hover]) if self.config.get_config_value('stats_metric') == 'tickets': metric_legend = 'Tickets' else: metric_legend = 'Story Points' # add renderers p.circle('x_data_dates', 'y_values', size=4, color='red', alpha=0.4, legend=metric_legend, source=source) p.line('x_data_dates', 'y_avg_values', color='blue', legend='4 Weeks Average', source=source) # NEW: customize by setting attributes p.title.text = 'Weekly Velocity' p.legend.location = 'top_left' p.grid.grid_line_alpha = 0 p.xaxis.axis_label = 'Weeks' p.yaxis.axis_label = metric_legend p.ygrid.band_fill_color = 'olive' p.ygrid.band_fill_alpha = 0.1 return p def get_points_per_type(self, stats_data): plot_values = [] for scan_day in stats_data: total_metric = stats_data[scan_day][self.config.get_config_value('stats_metric')] for assignee in stats_data[scan_day]['types']: plot_values.append([ stats_data[scan_day]['types'][assignee]['type'] , stats_data[scan_day]['types'][assignee][self.config.get_config_value('stats_metric')] , self.build_url_remaining('type', stats_data[scan_day]['types'][assignee]['type']) ]) break return plot_values, total_metric @staticmethod def get_remaining_source(dat): source = ColumnDataSource(dict( x_values=dat.entity , y_values=dat.value , jira_url=dat.jira_url )) return source def chart_remaining_types(self, stats_data): self.log.info('Generating graph about remaining effort per ticket type') plot_values, total_metric = self.get_points_per_type(stats_data) dat = pd.DataFrame(plot_values, columns=['entity', 'value', 'jira_url']) source = self.get_remaining_source(dat) # Declare tools hover = HoverTool( tooltips=[ ('Type:', '@x_values'), ('Points:', '@y_values') ] ) plot = figure( plot_width=600 , plot_height=300 , x_axis_label='Ticket Type' , y_axis_label=self.config.get_config_value('stats_metric') , title='Remaining ' + self.config.get_config_value('stats_metric') + ' per Ticket Type (Total: ' + str( total_metric) + ')' , x_range=FactorRange(factors=list(dat.entity)) , tools=['save', 'pan', 'box_zoom', 'reset', hover, 'tap'] ) taptool = plot.select(type=TapTool) taptool.callback = OpenURL(url='@jira_url') plot.vbar(source=source, x='x_values', top='y_values', bottom=0, width=0.3, color='green') return plot def get_points_per_assignee(self, stats_data): plot_values = [] for scan_day in stats_data: total_metric = stats_data[scan_day][self.config.get_config_value('stats_metric')] for assignee in stats_data[scan_day]['assignees']: plot_values.append([ stats_data[scan_day]['assignees'][assignee]['displayName'] , stats_data[scan_day]['assignees'][assignee][self.config.get_config_value('stats_metric')] , self.build_url_remaining('assignee', stats_data[scan_day]['assignees'][assignee]['displayName']) ]) break return plot_values, total_metric def chart_remaining_assignees(self, stats_data): self.log.info('Generating graph about remaining effort per assignee') plot_values, total_metric = self.get_points_per_assignee(stats_data) dat = pd.DataFrame(plot_values, columns=['entity', 'value', 'jira_url']) source = self.get_remaining_source(dat) # Declare tools hover = HoverTool( tooltips=[ ('Assignee:', '@x_values'), ('Points:', '@y_values') ] ) plot = figure( plot_width=600 , plot_height=300 , x_axis_label='Assignee' , y_axis_label=self.config.get_config_value('stats_metric') , title='Remaining ' + self.config.get_config_value('stats_metric') + ' per Jira Assignee (Total: ' + str( total_metric) + ')' , x_range=FactorRange(factors=list(dat.entity)) , tools=['save', 'pan', 'box_zoom', 'reset', hover, 'tap'] ) taptool = plot.select(type=TapTool) taptool.callback = OpenURL(url='@jira_url') plot.vbar(source=source, x='x_values', top='y_values', bottom=0, width=0.3, color='blue') return plot def chart_remaining_days(self, stats_data): self.log.info('Generating graph about estimated remaining days') plot_values = [] for scan_day in stats_data: for rol_avg in stats_data[scan_day]['days_to_completion']: if rol_avg == 'current': col_txt = 'This Week' elif rol_avg == 'all': col_txt = 'All Time' else: col_txt = str(rol_avg) + ' Weeks' plot_values.append([ col_txt , stats_data[scan_day]['days_to_completion'][rol_avg] ]) break dat = pd.DataFrame(plot_values, columns=['entity', 'value']) source = ColumnDataSource(dict( x_values=dat.entity , y_values=dat.value )) # Declare tools hover = HoverTool( tooltips=[ ('Estimated based on:', '@x_values'), ('Days to completion:', '@y_values') ] ) plot = figure( plot_width=600 , plot_height=300 , x_axis_label='Estimations calculated based on' , y_axis_label='Remaining days' , title='Estimated days to completion' , x_range=FactorRange(factors=list(dat.entity)) , tools=['save', 'pan', 'box_zoom', 'reset', hover] ) plot.vbar(source=source, x='x_values', top='y_values', bottom=0, width=0.3, color='red') return plot def main(self, stats_days, stats_weeks, stats_remaining): days_chart = self.build_velocity_days(stats_days) weeks_chart = self.build_velocity_weeks(stats_weeks) remaining_types = self.chart_remaining_types(stats_remaining) remaining_assignees = self.chart_remaining_assignees(stats_remaining) remaining_days = self.chart_remaining_days(stats_remaining) bokeh_layout = layout([ [days_chart] , [weeks_chart] , [remaining_types, remaining_assignees, remaining_days] ], sizing_mode='stretch_both') # output to static HTML file output_file(self.config.filepath_charts + 'index.html', title='[' + self.time.get_current_date().strftime( "%Y-%m-%d") + '] - Jira Metrics, built on: ' + self.time.get_current_date().strftime( "%Y-%m-%d")) save(bokeh_layout) show(bokeh_layout)
class PublishGithubPage(object): """ This class publish the chart to github pages """ def __init__(self, log, config): self.log = log self.config = config self.github = Github(self.log, self.config) self.time = Time(self.log, self.config) def main(self): """This hacky function publishes chart to github pages""" self.log.info('Publishing Website') self.log.info('Git local path: ' + self.config.get_config_value('git_localpath')) self.config.set_config_value( 'git_localpath', Files.prep_path(self.config.get_config_value('git_localpath'))) if not self.github.is_folder_git_repo(): self.log.info('Cloning Git Repository: ' + self.config.get_config_value('git_repo')) self.github.git_clone() else: self.log.info( 'Repository already available on local filesystem: ' + self.config.get_config_value('git_repo')) if self.github.git_pull() is not True: self.log.info('Git Stash') self.github.git_stash() # Test if requested branch exists, if not, create # Then switch to that branch remote_branches = self.github.get_branches() if not any( self.config.get_config_value('git_branch') in s for s in remote_branches): # Init branch self.log.warning( 'The remote repository does not contain branch: ' + self.config.get_config_value('git_branch')) self.github.git_checkout_create( self.config.get_config_value('git_branch')) self.github.git_pull_branch( self.config.get_config_value('git_branch'), True) self.github.git_push_branch( self.config.get_config_value('git_branch')) elif any('* ' + self.config.get_config_value('git_branch') in s for s in remote_branches): self.log.info('Local repository already selected branch: ' + self.config.get_config_value('git_branch')) else: self.log.info('Switching to branch: ' + self.config.get_config_value('git_branch')) self.github.git_checkout_f( self.config.get_config_value('git_branch')) # Next do one last pull and copy content self.github.git_stash() # Then copy chart to requested directory if not os.path.isfile(self.config.filepath_charts + 'index.html'): self.log.error('Unable to locate source chart file at: ' + self.config.filepath_charts + 'index.html') exit() dst_path = Files.prep_path( self.config.get_config_value('git_localpath') + self.config.get_config_value('git_pathdirectory')) self.log.info('Preparing to copy chart file to: ' + dst_path) shutil.copyfile(self.config.filepath_charts + 'index.html', dst_path + 'index.html') self.log.info('Chart file copied') self.github.git_add('--all') self.github.git_commit('Copied chart file - ' + self.time.get_current_date().isoformat()) self.github.git_push() self.log.info('Chart pushed to: ' + self.config.get_config_value('git_pageurl'))
def __init__(self, log, config): self.log = log self.config = config self.github = Github(self.log, self.config) self.time = Time(self.log, self.config)
def __init__(self, log, app_config): self.log = log self.config = Config(self.log) self.time = Time(self.log, self.config) self.log_config = LogConfig(self.log, app_config, self.config.config_path + 'load.log')
def test_get_current_date(self): answer_date = datetime.now( pytz.timezone('America/Toronto')).strftime('%Y-%m-%d') response_date = Time.get_current_date().strftime('%Y-%m-%d') self.assertEqual(answer_date, response_date)
def __init__(self, log, app_config, single_config='config.yml'): self.log = log self.config = Config(self.log, config_filename=single_config) self.time = Time(self.log, self.config) self.log_config = LogConfig(self.log, app_config, self.config.config_path + 'load.log')