| 
									
										
										
										
											2020-09-08 15:11:16 +02:00
										 |  |  | #!/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 matplotlib.dates | 
					
						
							|  |  |  | import matplotlib.patches as mpatches | 
					
						
							|  |  |  | import matplotlib.pyplot as plt | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  | import numpy as np | 
					
						
							|  |  |  | import requests | 
					
						
							| 
									
										
										
										
											2020-09-08 15:11:16 +02:00
										 |  |  | from dateutil import parser | 
					
						
							|  |  |  | from dateutil.relativedelta import relativedelta | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  | from matplotlib.dates import MONTHLY, DateFormatter, RRuleLocator, rrulewrapper | 
					
						
							| 
									
										
										
										
											2020-09-08 15:11:16 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 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: { | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |                 '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') | 
					
						
							| 
									
										
										
										
											2020-09-08 15:11:16 +02:00
										 |  |  |             } for x in self.sorted_releases_supported | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @staticmethod | 
					
						
							|  |  |  |     def parse_chart_releases_from_js(js_as_string): | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         return json.loads(js_as_string[js_as_string.find('RELEASES: ') + len('RELEASES: '):js_as_string.rfind('};')]) | 
					
						
							| 
									
										
										
										
											2020-09-08 15:11:16 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def _get_all_version_from_url(self, url=None, filename=None): | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         releases_file = requests.get(url).text if url is not None else ''.join(open(filename).readlines()) | 
					
						
							| 
									
										
										
										
											2020-09-08 15:11:16 +02:00
										 |  |  |         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) | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         formatter = DateFormatter('%b %Y') | 
					
						
							| 
									
										
										
										
											2020-09-08 15:11:16 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         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') | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         print('Saved into ' + output_chart_name + output_chart_extension) | 
					
						
							| 
									
										
										
										
											2020-09-08 15:11:16 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if __name__ == '__main__': | 
					
						
							|  |  |  |     arg_parser = argparse.ArgumentParser( | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         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') | 
					
						
							| 
									
										
										
										
											2020-09-08 15:11:16 +02:00
										 |  |  |     args = arg_parser.parse_args() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     ChartVersions(url=args.url if args.filename is None else None, filename=args.filename).create_chart( | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         output_chart_extension='.' + args.output_format.lower()[-3:], output_chart_name=args.output_file) |