#!/usr/bin/env python # Copyright 2020 Espressif Systems (Shanghai) PTE LTD # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # import argparse import datetime as dt import json import numpy as np import requests import matplotlib.dates import matplotlib.patches as mpatches import matplotlib.pyplot as plt from matplotlib.dates import MONTHLY, DateFormatter, RRuleLocator, rrulewrapper from dateutil import parser from dateutil.relativedelta import relativedelta class Version(object): def __init__(self, version_name, explicit_start_date, explicit_end_date, explicit_end_service_date=None): self.version_name = version_name self._start_date = parser.parse(explicit_start_date) self._end_of_life_date = parser.parse(explicit_end_date) self._end_service_date = parser.parse( explicit_end_service_date) if explicit_end_service_date is not None else self.compute_end_service_date() self.start_date_matplotlib_format = matplotlib.dates.date2num(self._start_date) self.end_of_life_date_matplotlib_format = matplotlib.dates.date2num(self._end_of_life_date) self.end_service_date_matplotlib_format = matplotlib.dates.date2num(self._end_service_date) @staticmethod def add_months(source_date, months): return source_date + relativedelta(months=+months) def get_start_date(self): return self._start_date def get_end_of_life_date(self): return self._end_of_life_date def get_end_service_date(self): return self._end_service_date def compute_end_service_date(self): return self.add_months(self._start_date, 12) class ChartVersions(object): def __init__(self, url=None, filename=None): self._releases = self._get_releases_from_url(url=url, filename=filename) self.sorted_releases_supported = sorted(self.filter_old_versions(self._releases), key=lambda x: x.version_name, reverse=True) def get_releases_as_json(self): return { x.version_name: { "start_date": x.get_start_date().strftime("%Y-%m-%d"), "end_service": x.get_end_service_date().strftime("%Y-%m-%d"), "end_date": x.get_end_of_life_date().strftime("%Y-%m-%d") } for x in self.sorted_releases_supported } @staticmethod def parse_chart_releases_from_js(js_as_string): return json.loads(js_as_string[js_as_string.find("RELEASES: ") + len("RELEASES: "):js_as_string.rfind("};")]) def _get_all_version_from_url(self, url=None, filename=None): releases_file = requests.get(url).text if url is not None else "".join(open(filename).readlines()) return self.parse_chart_releases_from_js(releases_file) def _get_releases_from_url(self, url=None, filename=None): all_versions = self._get_all_version_from_url(url, filename) return [ Version(version_name=x, explicit_start_date=all_versions[x]['start_date'], explicit_end_date=all_versions[x]['end_date'] if 'end_date' in all_versions[x].keys() else None, explicit_end_service_date=all_versions[x]['end_service'] if 'end_service' in all_versions[ x].keys() else None) for x in all_versions.keys() ] @staticmethod def filter_old_versions(versions): return list( filter(lambda x: x.get_end_of_life_date() >= dt.datetime.now(x.get_end_of_life_date().tzinfo), versions)) @staticmethod def months_timedelta(datetime_1, datetime2): datetime_1, datetime2 = (datetime2, datetime_1) if datetime_1 > datetime2 else (datetime_1, datetime2) return (datetime2.year * 12 + datetime2.month) - (datetime_1.year * 12 + datetime_1.month) @staticmethod def find_next_multiple_of_power_two(number, initial=3): """ Computes the next multiple of the number by some power of two. >>> ChartVersions.find_next_multiple_of_power_two(7, 3) 12 """ msb = number.bit_length() return 3 if number <= 1 else initial << msb - 2 << (1 & number >> msb - 2) def find_nearest_multiple_of_power_two(self, number, initial=3, prefer_next=False): next_num = self.find_next_multiple_of_power_two(number=number - 1, initial=initial) previous_num = next_num >> 1 return next_num if abs(next_num - number) < (abs(previous_num - number) + int(prefer_next)) else previous_num def create_chart(self, figure_size=(41.8330013267, 16.7332005307), subplot=111, step_size=0.5, bar_height=0.3, version_alpha=0.8, lts_service_color='darkred', lts_maintenance_color='red', bar_align='center', date_interval=None, output_chart_name='docs/chart', output_chart_extension='.png', months_surrounding_chart=4, service_period_label='Service period (Recommended for new designs)', maintenance_period_text='Maintenance period'): fig = plt.figure(figsize=figure_size) ax = fig.add_subplot(subplot) labels_count = len(self.sorted_releases_supported) pos = np.arange(step_size, labels_count * step_size + step_size, step_size) for release, i in zip(self.sorted_releases_supported, range(labels_count)): start_date = release.start_date_matplotlib_format end_of_service_date = release.end_service_date_matplotlib_format end_date = release.end_of_life_date_matplotlib_format ax.barh((i * step_size) + step_size, (end_of_service_date or end_date) - start_date, left=start_date, height=bar_height, align=bar_align, color=lts_service_color, alpha=version_alpha, edgecolor=lts_service_color) if end_of_service_date is not None: ax.barh((i * step_size) + step_size, end_date - end_of_service_date, left=end_of_service_date, height=bar_height, align=bar_align, color=lts_maintenance_color, alpha=version_alpha, edgecolor=lts_maintenance_color) ax.set_ylim(bottom=0, ymax=labels_count * step_size + step_size) max_ax_date = Version.add_months( max(self.sorted_releases_supported, key=lambda version: version.get_end_of_life_date().replace(tzinfo=None)).get_end_of_life_date(), months_surrounding_chart + 1).replace(day=1) min_ax_date = Version.add_months( min(self.sorted_releases_supported, key=lambda version: version.get_start_date().replace(tzinfo=None)).get_start_date(), -months_surrounding_chart).replace(day=1) x_ax_interval = date_interval or self.find_nearest_multiple_of_power_two( self.months_timedelta(max_ax_date, min_ax_date) // 10) ax.set_xlim(xmin=min_ax_date, xmax=max_ax_date) ax.grid(color='g', linestyle=':') ax.xaxis_date() rule = rrulewrapper(MONTHLY, interval=x_ax_interval) loc = RRuleLocator(rule) formatter = DateFormatter("%b %Y") ax.xaxis.set_major_locator(loc) ax.xaxis.set_major_formatter(formatter) x_labels = ax.get_xticklabels() plt.ylabel('ESP-IDF Release', size=12) ax.invert_yaxis() fig.autofmt_xdate() darkred_patch = mpatches.Patch(color=lts_service_color, label=service_period_label) red_patch = mpatches.Patch(color=lts_maintenance_color, label=maintenance_period_text) plt.setp(plt.yticks(pos, map(lambda x: x.version_name, self.sorted_releases_supported))[1], rotation=0, fontsize=10, family='Tahoma') plt.setp(x_labels, rotation=30, fontsize=11, family='Tahoma') plt.legend(handles=[darkred_patch, red_patch], prop={'size': 10, 'family': 'Tahoma'}, bbox_to_anchor=(1.01, 1.165), loc='upper right') fig.set_size_inches(11, 5, forward=True) plt.savefig(output_chart_name + output_chart_extension, bbox_inches='tight') print("Saved into " + output_chart_name + output_chart_extension) if __name__ == '__main__': arg_parser = argparse.ArgumentParser( description="Create chart of version support. Set the url or filename with versions." "If you set both filename and url the script will prefer filename.") arg_parser.add_argument("--url", metavar="URL", default="https://dl.espressif.com/dl/esp-idf/idf_versions.js") arg_parser.add_argument("--filename", help="Set the name of the source file, if is set, the script ignores the url.") arg_parser.add_argument("--output-format", help="Set the output format of the image.", default="svg") arg_parser.add_argument("--output-file", help="Set the name of the output file.", default="docs/chart") args = arg_parser.parse_args() ChartVersions(url=args.url if args.filename is None else None, filename=args.filename).create_chart( output_chart_extension="." + args.output_format.lower()[-3:], output_chart_name=args.output_file)