@@ -25,24 +25,64 @@ def parse_date(date_str: str) -> dt.date:
25
25
return dt .date .fromisoformat (date_str )
26
26
27
27
28
+ def parse_version (ver : str ) -> list [int ]:
29
+ return [int (i ) for i in ver ["key" ].split ("." )]
30
+
31
+
28
32
class Versions :
29
33
"""For converting JSON to CSV and SVG."""
30
34
31
- def __init__ (self ) -> None :
35
+ def __init__ (self , * , limit_to_active = False , special_py27 = False ) -> None :
32
36
with open ("include/release-cycle.json" , encoding = "UTF-8" ) as in_file :
33
37
self .versions = json .load (in_file )
34
38
35
39
# Generate a few additional fields
36
40
for key , version in self .versions .items ():
37
41
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 )
39
49
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
+
40
71
self .sorted_versions = sorted (
41
72
self .versions .values (),
42
- key = lambda v : [ int ( i ) for i in v [ "key" ]. split ( "." )] ,
73
+ key = parse_version ,
43
74
reverse = True ,
44
75
)
45
76
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
+
46
86
def write_csv (self ) -> None :
47
87
"""Output CSV files."""
48
88
now_str = str (dt .datetime .now (dt .timezone .utc ))
@@ -68,7 +108,7 @@ def write_csv(self) -> None:
68
108
csv_file .writeheader ()
69
109
csv_file .writerows (versions .values ())
70
110
71
- def write_svg (self , today : str ) -> None :
111
+ def write_svg (self , today : str , out_path : str ) -> None :
72
112
"""Output SVG file."""
73
113
env = jinja2 .Environment (
74
114
loader = jinja2 .FileSystemLoader ("_tools/" ),
@@ -85,6 +125,8 @@ def write_svg(self, today: str) -> None:
85
125
# CSS.
86
126
# (Ideally we'd actually use `em` units, but SVG viewBox doesn't take
87
127
# those.)
128
+
129
+ # Uppercase sizes are un-scaled
88
130
SCALE = 18
89
131
90
132
# Width of the drawing and main parts
@@ -96,7 +138,7 @@ def write_svg(self, today: str) -> None:
96
138
# some positioning numbers in the template as well.
97
139
LINE_HEIGHT = 1.5
98
140
99
- first_date = min ( ver [ "first_release_date" ] for ver in self .sorted_versions )
141
+ first_date = self .cutoff
100
142
last_date = max (ver ["end_of_life_date" ] for ver in self .sorted_versions )
101
143
102
144
def date_to_x (date : dt .date ) -> float :
@@ -105,7 +147,7 @@ def date_to_x(date: dt.date) -> float:
105
147
total_days = (last_date - first_date ).days
106
148
ratio = num_days / total_days
107
149
x = ratio * (DIAGRAM_WIDTH - LEGEND_WIDTH - RIGHT_MARGIN )
108
- return x + LEGEND_WIDTH
150
+ return ( x + LEGEND_WIDTH ) * SCALE
109
151
110
152
def year_to_x (year : int ) -> float :
111
153
"""Convert year number to an SVG X coordinate of 1st January"""
@@ -115,20 +157,21 @@ def format_year(year: int) -> str:
115
157
"""Format year number for display"""
116
158
return f"'{ year % 100 :02} "
117
159
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 :
121
161
template .stream (
122
162
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 ,
125
165
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 ,
127
169
versions = list (reversed (self .sorted_versions )),
128
170
today = dt .datetime .strptime (today , "%Y-%m-%d" ).date (),
129
171
year_to_x = year_to_x ,
130
172
date_to_x = date_to_x ,
131
173
format_year = format_year ,
174
+ id_key = self .id_key ,
132
175
).dump (f )
133
176
134
177
@@ -145,8 +188,12 @@ def main() -> None:
145
188
args = parser .parse_args ()
146
189
147
190
versions = Versions ()
191
+ assert len (versions .versions ) > 10
148
192
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" )
150
197
151
198
152
199
if __name__ == "__main__" :
0 commit comments