1
- """Read in a JSON and generate two CSVs and a Mermaid file."""
1
+ """Read in a JSON and generate two CSVs and a SVG file."""
2
2
from __future__ import annotations
3
3
4
4
import csv
5
5
import datetime as dt
6
6
import json
7
7
8
- MERMAID_HEADER = """
9
- gantt
10
- dateFormat YYYY-MM-DD
11
- title Python release cycle
12
- axisFormat %Y
13
- """ .lstrip ()
14
-
15
- MERMAID_SECTION = """
16
- section Python {version}
17
- {release_status} :{mermaid_status} python{version}, {first_release},{eol}
18
- """ # noqa: E501
19
-
20
- MERMAID_STATUS_MAPPING = {
21
- "feature" : "" ,
22
- "bugfix" : "active," ,
23
- "security" : "done," ,
24
- "end-of-life" : "crit," ,
25
- }
8
+ import jinja2
26
9
27
10
28
11
def csv_date (date_str : str , now_str : str ) -> str :
@@ -32,24 +15,27 @@ def csv_date(date_str: str, now_str: str) -> str:
32
15
return f"*{ date_str } *"
33
16
return date_str
34
17
35
-
36
- def mermaid_date (date_str : str ) -> str :
37
- """Format a date for Mermaid."""
18
+ def parse_date (date_str : str ) -> dt .date :
38
19
if len (date_str ) == len ("yyyy-mm" ):
39
- # Mermaid needs a full yyyy-mm-dd, so let's approximate
40
- date_str = f"{ date_str } -01"
41
- return date_str
42
-
20
+ # We need a full yyyy-mm-dd, so let's approximate
21
+ return dt .date .fromisoformat (date_str + '-01' )
22
+ return dt .date .fromisoformat (date_str )
43
23
44
24
class Versions :
45
- """For converting JSON to CSV and Mermaid ."""
25
+ """For converting JSON to CSV and SVG ."""
46
26
47
27
def __init__ (self ) -> None :
48
28
with open ("include/release-cycle.json" , encoding = "UTF-8" ) as in_file :
49
29
self .versions = json .load (in_file )
30
+
31
+ # Generate a few additional fields
32
+ for key , version in self .versions .items ():
33
+ version ['key' ] = key
34
+ version ['first_release_date' ] = parse_date (version ['first_release' ])
35
+ version ['end_of_life_date' ] = parse_date (version ['end_of_life' ])
50
36
self .sorted_versions = sorted (
51
- self .versions .items (),
52
- key = lambda k : [int (i ) for i in k [ 0 ].split ("." )],
37
+ self .versions .values (),
38
+ key = lambda v : [int (i ) for i in v [ 'key' ].split ("." )],
53
39
reverse = True ,
54
40
)
55
41
@@ -59,7 +45,7 @@ def write_csv(self) -> None:
59
45
60
46
versions_by_category = {"branches" : {}, "end-of-life" : {}}
61
47
headers = None
62
- for version , details in self .sorted_versions :
48
+ for details in self .sorted_versions :
63
49
row = {
64
50
"Branch" : details ["branch" ],
65
51
"Schedule" : f":pep:`{ details ['pep' ]} `" ,
@@ -70,38 +56,84 @@ def write_csv(self) -> None:
70
56
}
71
57
headers = row .keys ()
72
58
cat = "end-of-life" if details ["status" ] == "end-of-life" else "branches"
73
- versions_by_category [cat ][version ] = row
59
+ versions_by_category [cat ][details [ 'key' ] ] = row
74
60
75
61
for cat , versions in versions_by_category .items ():
76
62
with open (f"include/{ cat } .csv" , "w" , encoding = "UTF-8" , newline = "" ) as file :
77
63
csv_file = csv .DictWriter (file , fieldnames = headers , lineterminator = "\n " )
78
64
csv_file .writeheader ()
79
65
csv_file .writerows (versions .values ())
80
66
81
- def write_mermaid (self ) -> None :
82
- """Output Mermaid file."""
83
- out = [MERMAID_HEADER ]
84
-
85
- for version , details in reversed (self .versions .items ()):
86
- v = MERMAID_SECTION .format (
87
- version = version ,
88
- first_release = details ["first_release" ],
89
- eol = mermaid_date (details ["end_of_life" ]),
90
- release_status = details ["status" ],
91
- mermaid_status = MERMAID_STATUS_MAPPING [details ["status" ]],
92
- )
93
- out .append (v )
67
+ def write_svg (self ) -> None :
68
+ """Output SVG file."""
69
+ env = jinja2 .Environment (
70
+ loader = jinja2 .FileSystemLoader ('_tools/' ),
71
+ autoescape = True ,
72
+ undefined = jinja2 .StrictUndefined ,
73
+ )
74
+ template = env .get_template ("release_cycle_template.svg" )
75
+
76
+ # Scale. Should be roughly the pixel size of the font.
77
+ # All later sizes are miltiplied by this, so you can think of all other
78
+ # numbers being multiples of the font size, like using `em` units in
79
+ # CSS.
80
+ # (Ideally we'd actually use `em` units, but SVG viewBox doesn't take
81
+ # those.)
82
+ SCALE = 18
83
+
84
+ # Width of the drawing and main parts
85
+ DIAGRAM_WIDTH = 46
86
+ LEGEND_WIDTH = 7
87
+ RIGHT_MARGIN = 0.5
88
+
89
+ # Height of one line. If you change this you'll need to tweak
90
+ # some positioning nombers in the template as well.
91
+ LINE_HEIGHT = 1.5
92
+
93
+ first_date = min (
94
+ ver ['first_release_date' ] for ver in self .sorted_versions
95
+ )
96
+ last_date = max (
97
+ ver ['end_of_life_date' ] for ver in self .sorted_versions
98
+ ) + dt .timedelta (days = 10 * 365 )
99
+
100
+ def date_to_x (date ):
101
+ """Convert datetime.date to a SVG X coordinate"""
102
+ num_days = (date - first_date ).days
103
+ total_days = (last_date - first_date ).days
104
+ ratio = num_days / total_days
105
+ x = ratio * (DIAGRAM_WIDTH - LEGEND_WIDTH - RIGHT_MARGIN )
106
+ return x + LEGEND_WIDTH
107
+
108
+ def year_to_x (year ):
109
+ """Convert year number to a SVG X coordinate of 1st January"""
110
+ return date_to_x (dt .date (year , 1 , 1 ))
111
+
112
+ def format_year (year ):
113
+ """Format year number for display"""
114
+ return f"'{ year % 100 :02} "
94
115
95
116
with open (
96
- "include/release-cycle.mmd " , "w" , encoding = "UTF-8" , newline = " \n "
117
+ "include/release-cycle.svg " , "w" , encoding = "UTF-8" ,
97
118
) as f :
98
- f .writelines (out )
119
+ template .stream (
120
+ SCALE = SCALE ,
121
+ diagram_width = DIAGRAM_WIDTH ,
122
+ diagram_height = (len (self .sorted_versions ) + 2 ) * LINE_HEIGHT ,
123
+ years = range (first_date .year , last_date .year ),
124
+ LINE_HEIGHT = LINE_HEIGHT ,
125
+ versions = list (reversed (self .sorted_versions )),
126
+ today = dt .date .today (),
127
+ year_to_x = year_to_x ,
128
+ date_to_x = date_to_x ,
129
+ format_year = format_year ,
130
+ ).dump (f )
99
131
100
132
101
133
def main () -> None :
102
134
versions = Versions ()
103
135
versions .write_csv ()
104
- versions .write_mermaid ()
136
+ versions .write_svg ()
105
137
106
138
107
139
if __name__ == "__main__" :
0 commit comments