11import requests
2+ import json
23import collections
34from datetime import datetime , timedelta
45
56import pandas as pd
67from packaging .version import Version , InvalidVersion
78
89
9- py_releases = {
10+ PY_RELEASES = {
1011 "3.8" : "Oct 14, 2019" ,
1112 "3.9" : "Oct 5, 2020" ,
1213 "3.10" : "Oct 4, 2021" ,
1516 "3.13" : "Oct 7, 2024" ,
1617 "3.14" : "Oct 7, 2025" ,
1718}
18- core_packages = [
19- # Path(x).stem for x in glob("../core-projects/*.md") if "_index" not in x
20- "numpy" ,
21- "scipy" ,
19+ CORE_PACKAGES = [
20+ "ipython" ,
2221 "matplotlib" ,
22+ "networkx" ,
23+ "numpy" ,
2324 "pandas" ,
2425 "scikit-image" ,
25- "networkx" ,
2626 "scikit-learn" ,
27+ "scipy" ,
2728 "xarray" ,
28- "ipython" ,
2929 "zarr" ,
3030]
31- plus36 = timedelta (days = int (365 * 3 ))
32- plus24 = timedelta (days = int (365 * 2 ))
31+ PLUS_36_MONTHS = timedelta (days = int (365 * 3 ))
32+ PLUS_24_MONTHS = timedelta (days = int (365 * 2 ))
3333
3434# Release data
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
35+ # We put the cutoff at 3 quarters ago - we do not use "just" -9 months
36+ # to avoid the content of the quarter to change depending on when we
37+ # generate this file during the current quarter.
38+ CURRENT_DATE = pd .Timestamp .now ()
39+ CURRENT_QUARTER_START = pd .Timestamp (
40+ CURRENT_DATE .year , (CURRENT_DATE .quarter - 1 ) * 3 + 1 , 1
4341)
44- cutoff = current_quarter_start - pd .DateOffset (months = 9 )
42+ CUTOFF = CURRENT_QUARTER_START - pd .DateOffset (months = 9 )
4543
4644
47- def get_release_dates (package , support_time = plus24 ):
45+ def get_release_dates (package , support_time = PLUS_24_MONTHS ):
4846 releases = {}
49-
5047 print (f"Querying pypi.org for { package } versions..." , end = "" , flush = True )
5148 response = requests .get (
5249 f"https://pypi.org/simple/{ package } " ,
5350 headers = {"Accept" : "application/vnd.pypi.simple.v1+json" },
5451 ).json ()
5552 print ("OK" )
56-
5753 file_date = collections .defaultdict (list )
5854 for f in response ["files" ]:
55+ if f ["filename" ].endswith (".tar.gz" ) or f ["filename" ].endswith (".zip" ):
56+ continue
5957 ver = f ["filename" ].split ("-" )[1 ]
6058 try :
6159 version = Version (ver )
6260 except InvalidVersion as e :
6361 print (f"Error: '{ ver } ' is an invalid version for '{ package } '. Reason: { e } " )
6462 continue
65-
6663 if version .is_prerelease or version .micro != 0 :
6764 continue
68-
6965 release_date = None
7066 for format in ["%Y-%m-%dT%H:%M:%S.%fZ" , "%Y-%m-%dT%H:%M:%SZ" ]:
7167 try :
7268 release_date = datetime .strptime (f ["upload-time" ], format )
7369 except ValueError as e :
7470 print (f"Error parsing invalid date: { e } " )
75-
7671 if not release_date :
7772 continue
78-
7973 file_date [version ].append (release_date )
80-
8174 release_date = {v : min (file_date [v ]) for v in file_date }
82-
8375 for ver , release_date in sorted (release_date .items ()):
8476 drop_date = release_date + support_time
85- if drop_date >= cutoff :
77+ if drop_date >= CUTOFF :
8678 releases [ver ] = {
8779 "release_date" : release_date ,
8880 "drop_date" : drop_date ,
8981 }
90-
9182 return releases
9283
9384
9485package_releases = {
9586 "python" : {
9687 version : {
9788 "release_date" : datetime .strptime (release_date , "%b %d, %Y" ),
98- "drop_date" : datetime .strptime (release_date , "%b %d, %Y" ) + plus36 ,
89+ "drop_date" : datetime .strptime (release_date , "%b %d, %Y" ) + PLUS_36_MONTHS ,
9990 }
100- for version , release_date in py_releases .items ()
91+ for version , release_date in PY_RELEASES .items ()
10192 }
10293}
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
94+ package_releases |= {package : get_release_dates (package ) for package in CORE_PACKAGES }
95+ # Filter all items whose drop_date are in the past
10796package_releases = {
10897 package : {
10998 version : dates
11099 for version , dates in releases .items ()
111- if dates ["drop_date" ] > cutoff
100+ if dates ["drop_date" ] > CUTOFF
112101 }
113102 for package , releases in package_releases .items ()
114103}
115104
116-
117105# Save Gantt chart
118-
119106print ("Saving Mermaid chart to chart.md" )
120107with open ("chart.md" , "w" ) as fh :
121108 fh .write (
@@ -124,7 +111,6 @@ def get_release_dates(package, support_time=plus24):
124111axisFormat %m / %Y
125112title Support Window"""
126113 )
127-
128114 for name , releases in package_releases .items ():
129115 fh .write (f"\n \n section { name } " )
130116 for version , dates in releases .items ():
@@ -134,7 +120,6 @@ def get_release_dates(package, support_time=plus24):
134120 fh .write ("\n " )
135121
136122# Print drop schedule
137-
138123data = []
139124for k , versions in package_releases .items ():
140125 for v , dates in versions .items ():
@@ -146,15 +131,30 @@ def get_release_dates(package, support_time=plus24):
146131 pd .to_datetime (dates ["drop_date" ]),
147132 )
148133 )
149-
150134df = pd .DataFrame (data , columns = ["package" , "version" , "release" , "drop" ])
151-
152135df ["quarter" ] = df ["drop" ].dt .to_period ("Q" )
136+ df ["new_min_version" ] = (
137+ df [["package" , "version" , "quarter" ]].groupby ("package" ).shift (- 1 )["version" ]
138+ )
139+ dq = df .set_index (["quarter" , "package" ]).sort_index ().dropna ()
140+ new_min_versions = (
141+ dq .groupby (["quarter" , "package" ]).agg ({"new_min_version" : "max" }).reset_index ()
142+ )
153143
154- dq = df .set_index (["quarter" , "package" ]).sort_index ()
155-
156-
157- print ("Saving drop schedule to schedule.md" )
144+ # We want to build a dict with the structure [{start_date: timestamp, packages: {package: lower_bound}}]
145+ new_min_versions_list = []
146+ for q , packages in new_min_versions .groupby ("quarter" ):
147+ package_lower_bounds = {
148+ p : str (v ) for p , v in packages .drop ("quarter" , axis = 1 ).itertuples (index = False )
149+ }
150+ # jq is really insistent the Z should be there
151+ quarter_start_time_str = str (q .start_time .isoformat ()) + "Z"
152+ new_min_versions_list .append (
153+ {"start_date" : quarter_start_time_str , "packages" : package_lower_bounds }
154+ )
155+ print ("Saving drop schedule to schedule.json" )
156+ with open ("schedule.json" , "w" ) as f :
157+ f .write (json .dumps (new_min_versions_list , sort_keys = True ))
158158
159159
160160def pad_table (table ):
@@ -172,7 +172,6 @@ def pad_table(table):
172172 line += f"| { str .ljust (entry , width )} "
173173 line += "|"
174174 padded_table .append (line )
175-
176175 return padded_table
177176
178177
@@ -192,7 +191,6 @@ def make_table(sub):
192191 else f"{ rel_min .strftime ('%b %Y' )} and { rel_max .strftime ('%b %Y' )} "
193192 )
194193 table .append (f"|{ package :<15} |{ version_range :<19} |released { rel_range } |" )
195-
196194 return pad_table (table )
197195
198196
@@ -204,13 +202,13 @@ def make_quarter(quarter, dq):
204202 return "\n " .join (table )
205203
206204
205+ print ("Saving drop schedule to schedule.md" )
207206with open ("schedule.md" , "w" ) as fh :
208- # we collect package 6 month in the past, and drop the first quarter
207+ # We collect packages 6 month in the past, and drop the first quarter
209208 # as we might have filtered some of the packages out depending on
210209 # when we ran the script.
211210 tb = []
212211 for quarter in list (sorted (set (dq .index .get_level_values (0 ))))[1 :]:
213212 tb .append (make_quarter (quarter , dq ))
214-
215213 fh .write ("\n \n " .join (tb ))
216214 fh .write ("\n " )
0 commit comments