11import requests
2- import json
32import collections
43from datetime import datetime , timedelta
54
65import pandas as pd
7- from packaging .version import Version
6+ from packaging .version import Version , InvalidVersion
87
98
10- PY_RELEASES = {
9+ py_releases = {
1110 "3.8" : "Oct 14, 2019" ,
1211 "3.9" : "Oct 5, 2020" ,
1312 "3.10" : "Oct 4, 2021" ,
1413 "3.11" : "Oct 24, 2022" ,
1514 "3.12" : "Oct 2, 2023" ,
1615 "3.13" : "Oct 7, 2024" ,
16+ "3.14" : "Oct 7, 2025" ,
1717}
18- CORE_PACKAGES = [
19- "ipython" ,
20- "matplotlib" ,
21- "networkx" ,
18+ core_packages = [
19+ # Path(x).stem for x in glob("../core-projects/*.md") if "_index" not in x
2220 "numpy" ,
21+ "scipy" ,
22+ "matplotlib" ,
2323 "pandas" ,
2424 "scikit-image" ,
25+ "networkx" ,
2526 "scikit-learn" ,
26- "scipy" ,
2727 "xarray" ,
28+ "ipython" ,
2829 "zarr" ,
2930]
30- PLUS_36_MONTHS = timedelta (days = int (365 * 3 ))
31- PLUS_24_MONTHS = timedelta (days = int (365 * 2 ))
31+ plus36 = timedelta (days = int (365 * 3 ))
32+ plus24 = timedelta (days = int (365 * 2 ))
3233
3334# Release data
34- # We put the cutoff at 3 quarters ago - we do not use "just" -9 months
35- # to avoid the content of the quarter to change depending on when we
36- # generate this file during the current quarter.
37- CURRENT_DATE = pd .Timestamp .now ()
38- CURRENT_QUARTER_START = pd .Timestamp (
39- CURRENT_DATE .year , (CURRENT_DATE .quarter - 1 ) * 3 + 1 , 1
35+
36+ # put cutoff 3 quarters ago – we do not use "just" -9 month,
37+ # to avoid the content of the quarter to change depending on when we generate this
38+ # file during the current quarter.
39+
40+ current_date = pd .Timestamp .now ()
41+ current_quarter_start = pd .Timestamp (
42+ current_date .year , (current_date .quarter - 1 ) * 3 + 1 , 1
4043)
41- CUTOFF = CURRENT_QUARTER_START - pd .DateOffset (months = 9 )
44+ cutoff = current_quarter_start - pd .DateOffset (months = 9 )
4245
4346
44- def get_release_dates (package , support_time = PLUS_24_MONTHS ):
47+ def get_release_dates (package , support_time = plus24 ):
4548 releases = {}
49+
4650 print (f"Querying pypi.org for { package } versions..." , end = "" , flush = True )
4751 response = requests .get (
4852 f"https://pypi.org/simple/{ package } " ,
4953 headers = {"Accept" : "application/vnd.pypi.simple.v1+json" },
5054 ).json ()
5155 print ("OK" )
56+
5257 file_date = collections .defaultdict (list )
5358 for f in response ["files" ]:
54- if f ["filename" ].endswith (".tar.gz" ) or f ["filename" ].endswith (".zip" ):
55- continue
5659 ver = f ["filename" ].split ("-" )[1 ]
5760 try :
5861 version = Version (ver )
59- except Exception :
62+ except InvalidVersion as e :
63+ print (f"Error: '{ ver } ' is an invalid version for '{ package } '. Reason: { e } " )
6064 continue
65+
6166 if version .is_prerelease or version .micro != 0 :
6267 continue
63- release_date = pd .Timestamp (f ["upload-time" ]).tz_localize (None )
68+
69+ release_date = None
70+ for format in ["%Y-%m-%dT%H:%M:%S.%fZ" , "%Y-%m-%dT%H:%M:%SZ" ]:
71+ try :
72+ release_date = datetime .strptime (f ["upload-time" ], format )
73+ except ValueError as e :
74+ print (f"Error parsing invalid date: { e } " )
75+
6476 if not release_date :
6577 continue
78+
6679 file_date [version ].append (release_date )
80+
6781 release_date = {v : min (file_date [v ]) for v in file_date }
82+
6883 for ver , release_date in sorted (release_date .items ()):
6984 drop_date = release_date + support_time
70- if drop_date >= CUTOFF :
85+ if drop_date >= cutoff :
7186 releases [ver ] = {
7287 "release_date" : release_date ,
7388 "drop_date" : drop_date ,
7489 }
90+
7591 return releases
7692
7793
7894package_releases = {
7995 "python" : {
8096 version : {
8197 "release_date" : datetime .strptime (release_date , "%b %d, %Y" ),
82- "drop_date" : datetime .strptime (release_date , "%b %d, %Y" ) + PLUS_36_MONTHS ,
98+ "drop_date" : datetime .strptime (release_date , "%b %d, %Y" ) + plus36 ,
8399 }
84- for version , release_date in PY_RELEASES .items ()
100+ for version , release_date in py_releases .items ()
85101 }
86102}
87- package_releases |= {package : get_release_dates (package ) for package in CORE_PACKAGES }
88- # Filter all items whose drop_date are in the past
103+
104+ package_releases |= {package : get_release_dates (package ) for package in core_packages }
105+
106+ # filter all items whose drop_date are in the past
89107package_releases = {
90108 package : {
91109 version : dates
92110 for version , dates in releases .items ()
93- if dates ["drop_date" ] > CUTOFF
111+ if dates ["drop_date" ] > cutoff
94112 }
95113 for package , releases in package_releases .items ()
96114}
97115
116+
98117# Save Gantt chart
118+
99119print ("Saving Mermaid chart to chart.md" )
100120with open ("chart.md" , "w" ) as fh :
101121 fh .write (
@@ -104,6 +124,7 @@ def get_release_dates(package, support_time=PLUS_24_MONTHS):
104124axisFormat %m / %Y
105125title Support Window"""
106126 )
127+
107128 for name , releases in package_releases .items ():
108129 fh .write (f"\n \n section { name } " )
109130 for version , dates in releases .items ():
@@ -113,6 +134,7 @@ def get_release_dates(package, support_time=PLUS_24_MONTHS):
113134 fh .write ("\n " )
114135
115136# Print drop schedule
137+
116138data = []
117139for k , versions in package_releases .items ():
118140 for v , dates in versions .items ():
@@ -124,30 +146,15 @@ def get_release_dates(package, support_time=PLUS_24_MONTHS):
124146 pd .to_datetime (dates ["drop_date" ]),
125147 )
126148 )
149+
127150df = pd .DataFrame (data , columns = ["package" , "version" , "release" , "drop" ])
151+
128152df ["quarter" ] = df ["drop" ].dt .to_period ("Q" )
129- df ["new_min_version" ] = (
130- df [["package" , "version" , "quarter" ]].groupby ("package" ).shift (- 1 )["version" ]
131- )
132- dq = df .set_index (["quarter" , "package" ]).sort_index ().dropna ()
133- new_min_versions = (
134- dq .groupby (["quarter" , "package" ]).agg ({"new_min_version" : "max" }).reset_index ()
135- )
136153
137- # We want to build a dict with the structure [{start_date: timestamp, packages: {package: lower_bound}}]
138- new_min_versions_list = []
139- for q , packages in new_min_versions .groupby ("quarter" ):
140- package_lower_bounds = {
141- p : str (v ) for p , v in packages .drop ("quarter" , axis = 1 ).itertuples (index = False )
142- }
143- # jq is really insistent the Z should be there
144- quarter_start_time_str = str (q .start_time .isoformat ()) + "Z"
145- new_min_versions_list .append (
146- {"start_date" : quarter_start_time_str , "packages" : package_lower_bounds }
147- )
148- print ("Saving drop schedule to schedule.json" )
149- with open ("schedule.json" , "w" ) as f :
150- f .write (json .dumps (new_min_versions_list , sort_keys = True ))
154+ dq = df .set_index (["quarter" , "package" ]).sort_index ()
155+
156+
157+ print ("Saving drop schedule to schedule.md" )
151158
152159
153160def pad_table (table ):
@@ -165,6 +172,7 @@ def pad_table(table):
165172 line += f"| { str .ljust (entry , width )} "
166173 line += "|"
167174 padded_table .append (line )
175+
168176 return padded_table
169177
170178
@@ -184,6 +192,7 @@ def make_table(sub):
184192 else f"{ rel_min .strftime ('%b %Y' )} and { rel_max .strftime ('%b %Y' )} "
185193 )
186194 table .append (f"|{ package :<15} |{ version_range :<19} |released { rel_range } |" )
195+
187196 return pad_table (table )
188197
189198
@@ -195,13 +204,13 @@ def make_quarter(quarter, dq):
195204 return "\n " .join (table )
196205
197206
198- print ("Saving drop schedule to schedule.md" )
199207with open ("schedule.md" , "w" ) as fh :
200- # We collect packages 6 month in the past, and drop the first quarter
208+ # we collect package 6 month in the past, and drop the first quarter
201209 # as we might have filtered some of the packages out depending on
202210 # when we ran the script.
203211 tb = []
204212 for quarter in list (sorted (set (dq .index .get_level_values (0 ))))[1 :]:
205213 tb .append (make_quarter (quarter , dq ))
214+
206215 fh .write ("\n \n " .join (tb ))
207216 fh .write ("\n " )
0 commit comments