@@ -25,24 +25,64 @@ def parse_date(date_str: str) -> dt.date:
2525 return dt .date .fromisoformat (date_str )
2626
2727
28+ def parse_version (ver : str ) -> list [int ]:
29+ return [int (i ) for i in ver ["key" ].split ("." )]
30+
31+
2832class Versions :
2933 """For converting JSON to CSV and SVG."""
3034
31- def __init__ (self ) -> None :
35+ def __init__ (self , * , limit_to_active = False , special_py27 = False ) -> None :
3236 with open ("include/release-cycle.json" , encoding = "UTF-8" ) as in_file :
3337 self .versions = json .load (in_file )
3438
3539 # Generate a few additional fields
3640 for key , version in self .versions .items ():
3741 version ["key" ] = key
38- version ["first_release_date" ] = parse_date (version ["first_release" ])
42+ ver_info = parse_version (version )
43+ if ver_info >= [3 , 13 ]:
44+ full_years = 2
45+ else :
46+ full_years = 1.5
47+ version ["first_release_date" ] = r1 = parse_date (version ["first_release" ])
48+ version ["start_security_date" ] = r1 + dt .timedelta (days = full_years * 365 )
3949 version ["end_of_life_date" ] = parse_date (version ["end_of_life" ])
50+
51+ self .cutoff = min (ver ["first_release_date" ] for ver in self .versions .values ())
52+
53+ if limit_to_active :
54+ self .cutoff = min (
55+ version ["first_release_date" ]
56+ for version in self .versions .values ()
57+ if version ["status" ] != "end-of-life"
58+ )
59+ self .versions = {
60+ key : version
61+ for key , version in self .versions .items ()
62+ if version ["end_of_life_date" ] >= self .cutoff
63+ or (special_py27 and key == "2.7" )
64+ }
65+ if special_py27 :
66+ self .cutoff = min (self .cutoff , dt .date (2019 , 8 , 1 ))
67+ self .id_key = "active"
68+ else :
69+ self .id_key = "all"
70+
4071 self .sorted_versions = sorted (
4172 self .versions .values (),
42- key = lambda v : [ int ( i ) for i in v [ "key" ]. split ( "." )] ,
73+ key = parse_version ,
4374 reverse = True ,
4475 )
4576
77+ # Set the row (Y coordinate) for the chart, to allow a gap between 2.7
78+ # and the rest
79+ y = len (self .sorted_versions ) + (1 if special_py27 else 0 )
80+ for version in self .sorted_versions :
81+ if special_py27 and version ["key" ] == "2.7" :
82+ y -= 1
83+ version ["y" ] = y
84+ y -= 1
85+
4686 def write_csv (self ) -> None :
4787 """Output CSV files."""
4888 now_str = str (dt .datetime .now (dt .timezone .utc ))
@@ -68,7 +108,7 @@ def write_csv(self) -> None:
68108 csv_file .writeheader ()
69109 csv_file .writerows (versions .values ())
70110
71- def write_svg (self , today : str ) -> None :
111+ def write_svg (self , today : str , out_path : str ) -> None :
72112 """Output SVG file."""
73113 env = jinja2 .Environment (
74114 loader = jinja2 .FileSystemLoader ("_tools/" ),
@@ -85,6 +125,8 @@ def write_svg(self, today: str) -> None:
85125 # CSS.
86126 # (Ideally we'd actually use `em` units, but SVG viewBox doesn't take
87127 # those.)
128+
129+ # Uppercase sizes are un-scaled
88130 SCALE = 18
89131
90132 # Width of the drawing and main parts
@@ -96,7 +138,7 @@ def write_svg(self, today: str) -> None:
96138 # some positioning numbers in the template as well.
97139 LINE_HEIGHT = 1.5
98140
99- first_date = min ( ver [ "first_release_date" ] for ver in self .sorted_versions )
141+ first_date = self .cutoff
100142 last_date = max (ver ["end_of_life_date" ] for ver in self .sorted_versions )
101143
102144 def date_to_x (date : dt .date ) -> float :
@@ -105,7 +147,7 @@ def date_to_x(date: dt.date) -> float:
105147 total_days = (last_date - first_date ).days
106148 ratio = num_days / total_days
107149 x = ratio * (DIAGRAM_WIDTH - LEGEND_WIDTH - RIGHT_MARGIN )
108- return x + LEGEND_WIDTH
150+ return ( x + LEGEND_WIDTH ) * SCALE
109151
110152 def year_to_x (year : int ) -> float :
111153 """Convert year number to an SVG X coordinate of 1st January"""
@@ -115,20 +157,21 @@ def format_year(year: int) -> str:
115157 """Format year number for display"""
116158 return f"'{ year % 100 :02} "
117159
118- with open (
119- "include/release-cycle.svg" , "w" , encoding = "UTF-8" , newline = "\n "
120- ) as f :
160+ with open (out_path , "w" , encoding = "UTF-8" , newline = "\n " ) as f :
121161 template .stream (
122162 SCALE = SCALE ,
123- diagram_width = DIAGRAM_WIDTH ,
124- diagram_height = (len ( self .sorted_versions ) + 2 ) * LINE_HEIGHT ,
163+ diagram_width = DIAGRAM_WIDTH * SCALE ,
164+ diagram_height = (self .sorted_versions [ 0 ][ "y" ] + 2 ) * LINE_HEIGHT * SCALE ,
125165 years = range (first_date .year , last_date .year + 1 ),
126- LINE_HEIGHT = LINE_HEIGHT ,
166+ line_height = LINE_HEIGHT * SCALE ,
167+ legend_width = LEGEND_WIDTH * SCALE ,
168+ right_margin = RIGHT_MARGIN * SCALE ,
127169 versions = list (reversed (self .sorted_versions )),
128170 today = dt .datetime .strptime (today , "%Y-%m-%d" ).date (),
129171 year_to_x = year_to_x ,
130172 date_to_x = date_to_x ,
131173 format_year = format_year ,
174+ id_key = self .id_key ,
132175 ).dump (f )
133176
134177
@@ -145,8 +188,12 @@ def main() -> None:
145188 args = parser .parse_args ()
146189
147190 versions = Versions ()
191+ assert len (versions .versions ) > 10
148192 versions .write_csv ()
149- versions .write_svg (args .today )
193+ versions .write_svg (args .today , "include/release-cycle-all.svg" )
194+
195+ versions = Versions (limit_to_active = True , special_py27 = True )
196+ versions .write_svg (args .today , "include/release-cycle.svg" )
150197
151198
152199if __name__ == "__main__" :
0 commit comments