1#!/usr/bin/env python 2 3# Copyright 2020 Espressif Systems (Shanghai) PTE LTD 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16# 17import argparse 18import datetime as dt 19import json 20 21import matplotlib.dates 22import matplotlib.patches as mpatches 23import matplotlib.pyplot as plt 24import numpy as np 25import requests 26from dateutil import parser 27from dateutil.relativedelta import relativedelta 28from matplotlib.dates import MONTHLY, DateFormatter, RRuleLocator, rrulewrapper 29 30 31class Version(object): 32 def __init__(self, version_name, explicit_start_date, explicit_end_date, explicit_end_service_date=None): 33 self.version_name = version_name 34 35 self._start_date = parser.parse(explicit_start_date) 36 self._end_of_life_date = parser.parse(explicit_end_date) 37 self._end_service_date = parser.parse( 38 explicit_end_service_date) if explicit_end_service_date is not None else self.compute_end_service_date() 39 40 self.start_date_matplotlib_format = matplotlib.dates.date2num(self._start_date) 41 self.end_of_life_date_matplotlib_format = matplotlib.dates.date2num(self._end_of_life_date) 42 43 self.end_service_date_matplotlib_format = matplotlib.dates.date2num(self._end_service_date) 44 45 @staticmethod 46 def add_months(source_date, months): 47 return source_date + relativedelta(months=+months) 48 49 def get_start_date(self): 50 return self._start_date 51 52 def get_end_of_life_date(self): 53 return self._end_of_life_date 54 55 def get_end_service_date(self): 56 return self._end_service_date 57 58 def compute_end_service_date(self): 59 return self.add_months(self._start_date, 12) 60 61 62class ChartVersions(object): 63 def __init__(self, url=None, filename=None): 64 self._releases = self._get_releases_from_url(url=url, filename=filename) 65 self.sorted_releases_supported = sorted(self.filter_old_versions(self._releases), key=lambda x: x.version_name, 66 reverse=True) 67 68 def get_releases_as_json(self): 69 return { 70 x.version_name: { 71 'start_date': x.get_start_date().strftime('%Y-%m-%d'), 72 'end_service': x.get_end_service_date().strftime('%Y-%m-%d'), 73 'end_date': x.get_end_of_life_date().strftime('%Y-%m-%d') 74 } for x in self.sorted_releases_supported 75 } 76 77 @staticmethod 78 def parse_chart_releases_from_js(js_as_string): 79 return json.loads(js_as_string[js_as_string.find('RELEASES: ') + len('RELEASES: '):js_as_string.rfind('};')]) 80 81 def _get_all_version_from_url(self, url=None, filename=None): 82 releases_file = requests.get(url).text if url is not None else ''.join(open(filename).readlines()) 83 return self.parse_chart_releases_from_js(releases_file) 84 85 def _get_releases_from_url(self, url=None, filename=None): 86 all_versions = self._get_all_version_from_url(url, filename) 87 return [ 88 Version(version_name=x, 89 explicit_start_date=all_versions[x]['start_date'], 90 explicit_end_date=all_versions[x]['end_date'] if 'end_date' in all_versions[x].keys() else None, 91 explicit_end_service_date=all_versions[x]['end_service'] if 'end_service' in all_versions[ 92 x].keys() else None) 93 for x in all_versions.keys() 94 ] 95 96 @staticmethod 97 def filter_old_versions(versions): 98 return list( 99 filter(lambda x: x.get_end_of_life_date() >= dt.datetime.now(x.get_end_of_life_date().tzinfo), versions)) 100 101 @staticmethod 102 def months_timedelta(datetime_1, datetime2): 103 datetime_1, datetime2 = (datetime2, datetime_1) if datetime_1 > datetime2 else (datetime_1, datetime2) 104 return (datetime2.year * 12 + datetime2.month) - (datetime_1.year * 12 + datetime_1.month) 105 106 @staticmethod 107 def find_next_multiple_of_power_two(number, initial=3): 108 """ 109 Computes the next multiple of the number by some power of two. 110 >>> ChartVersions.find_next_multiple_of_power_two(7, 3) 111 12 112 """ 113 msb = number.bit_length() 114 return 3 if number <= 1 else initial << msb - 2 << (1 & number >> msb - 2) 115 116 def find_nearest_multiple_of_power_two(self, number, initial=3, prefer_next=False): 117 next_num = self.find_next_multiple_of_power_two(number=number - 1, initial=initial) 118 previous_num = next_num >> 1 119 return next_num if abs(next_num - number) < (abs(previous_num - number) + int(prefer_next)) else previous_num 120 121 def create_chart(self, 122 figure_size=(41.8330013267, 16.7332005307), 123 subplot=111, 124 step_size=0.5, 125 bar_height=0.3, 126 version_alpha=0.8, 127 lts_service_color='darkred', 128 lts_maintenance_color='red', 129 bar_align='center', 130 date_interval=None, 131 output_chart_name='docs/chart', 132 output_chart_extension='.png', 133 months_surrounding_chart=4, 134 service_period_label='Service period (Recommended for new designs)', 135 maintenance_period_text='Maintenance period'): 136 fig = plt.figure(figsize=figure_size) 137 ax = fig.add_subplot(subplot) 138 139 labels_count = len(self.sorted_releases_supported) 140 141 pos = np.arange(step_size, labels_count * step_size + step_size, step_size) 142 143 for release, i in zip(self.sorted_releases_supported, range(labels_count)): 144 start_date = release.start_date_matplotlib_format 145 end_of_service_date = release.end_service_date_matplotlib_format 146 147 end_date = release.end_of_life_date_matplotlib_format 148 149 ax.barh((i * step_size) + step_size, (end_of_service_date or end_date) - start_date, left=start_date, 150 height=bar_height, align=bar_align, 151 color=lts_service_color, 152 alpha=version_alpha, 153 edgecolor=lts_service_color) 154 if end_of_service_date is not None: 155 ax.barh((i * step_size) + step_size, end_date - end_of_service_date, left=end_of_service_date, 156 height=bar_height, align=bar_align, 157 color=lts_maintenance_color, alpha=version_alpha, edgecolor=lts_maintenance_color) 158 159 ax.set_ylim(bottom=0, ymax=labels_count * step_size + step_size) 160 161 max_ax_date = Version.add_months( 162 max(self.sorted_releases_supported, 163 key=lambda version: version.get_end_of_life_date().replace(tzinfo=None)).get_end_of_life_date(), 164 months_surrounding_chart + 1).replace(day=1) 165 166 min_ax_date = Version.add_months( 167 min(self.sorted_releases_supported, 168 key=lambda version: version.get_start_date().replace(tzinfo=None)).get_start_date(), 169 -months_surrounding_chart).replace(day=1) 170 171 x_ax_interval = date_interval or self.find_nearest_multiple_of_power_two( 172 self.months_timedelta(max_ax_date, min_ax_date) // 10) 173 174 ax.set_xlim(xmin=min_ax_date, xmax=max_ax_date) 175 176 ax.grid(color='g', linestyle=':') 177 ax.xaxis_date() 178 179 rule = rrulewrapper(MONTHLY, interval=x_ax_interval) 180 loc = RRuleLocator(rule) 181 formatter = DateFormatter('%b %Y') 182 183 ax.xaxis.set_major_locator(loc) 184 ax.xaxis.set_major_formatter(formatter) 185 x_labels = ax.get_xticklabels() 186 plt.ylabel('ESP-IDF Release', size=12) 187 188 ax.invert_yaxis() 189 fig.autofmt_xdate() 190 191 darkred_patch = mpatches.Patch(color=lts_service_color, label=service_period_label) 192 red_patch = mpatches.Patch(color=lts_maintenance_color, label=maintenance_period_text) 193 194 plt.setp(plt.yticks(pos, map(lambda x: x.version_name, self.sorted_releases_supported))[1], rotation=0, 195 fontsize=10, family='Tahoma') 196 plt.setp(x_labels, rotation=30, fontsize=11, family='Tahoma') 197 plt.legend(handles=[darkred_patch, red_patch], prop={'size': 10, 'family': 'Tahoma'}, 198 bbox_to_anchor=(1.01, 1.165), loc='upper right') 199 fig.set_size_inches(11, 5, forward=True) 200 plt.savefig(output_chart_name + output_chart_extension, bbox_inches='tight') 201 print('Saved into ' + output_chart_name + output_chart_extension) 202 203 204if __name__ == '__main__': 205 arg_parser = argparse.ArgumentParser( 206 description='Create chart of version support. Set the url or filename with versions.' 207 'If you set both filename and url the script will prefer filename.') 208 arg_parser.add_argument('--url', metavar='URL', default='https://dl.espressif.com/dl/esp-idf/idf_versions.js') 209 arg_parser.add_argument('--filename', 210 help='Set the name of the source file, if is set, the script ignores the url.') 211 arg_parser.add_argument('--output-format', help='Set the output format of the image.', default='svg') 212 arg_parser.add_argument('--output-file', help='Set the name of the output file.', default='docs/chart') 213 args = arg_parser.parse_args() 214 215 ChartVersions(url=args.url if args.filename is None else None, filename=args.filename).create_chart( 216 output_chart_extension='.' + args.output_format.lower()[-3:], output_chart_name=args.output_file) 217