44import requests
55from django import template
66from django .core .cache import cache
7+ from django .utils .html import format_html
8+
9+ from downloads .models import Release
710
811register = template .Library ()
912logger = logging .getLogger (__name__ )
1013
11- PYTHON_RELEASES_URL = "https://peps.python.org/api/python-releases.json"
12- PYTHON_RELEASES_CACHE_KEY = "python_python_releases"
13- PYTHON_RELEASES_CACHE_TIMEOUT = 3600 # 1 hour
14-
15-
16- def get_python_releases_data () -> dict | None :
17- """Fetch and cache the Python release cycle data from PEPs API."""
18- data = cache .get (PYTHON_RELEASES_CACHE_KEY )
19- if data is not None :
20- return data
21-
22- try :
23- response = requests .get (PYTHON_RELEASES_URL , timeout = 5 )
24- response .raise_for_status ()
25- data = response .json ()
26- cache .set (PYTHON_RELEASES_CACHE_KEY , data , PYTHON_RELEASES_CACHE_TIMEOUT )
27- return data
28- except (requests .RequestException , ValueError ) as e :
29- logger .warning ("Failed to fetch release cycle data: %s" , e )
30- return None
14+ RELEASE_CYCLE_URL = "https://peps.python.org/api/release-cycle.json"
15+ RELEASE_CYCLE_CACHE_KEY = "python_release_cycle"
16+ RELEASE_CYCLE_CACHE_TIMEOUT = 3600 # 1 hour
3117
3218
3319@register .simple_tag
@@ -52,14 +38,12 @@ def get_eol_info(release) -> dict:
5238 major = int (match .group (1 ))
5339 minor_version = f"{ match .group (1 )} .{ match .group (2 )} "
5440
55- python_releases = get_python_releases_data ()
56- if python_releases is None :
41+ release_cycle = get_release_cycle_data ()
42+ if release_cycle is None :
5743 # Can't determine EOL status, don't show warning
5844 return result
5945
60- metadata = python_releases .get ("metadata" , {})
61- version_info = metadata .get (minor_version )
62-
46+ version_info = release_cycle .get (minor_version )
6347 if version_info is None :
6448 # Python 2 releases not in the list are EOL
6549 if major <= 2 :
@@ -96,6 +80,16 @@ def has_sbom(files):
9680 return any (f .sbom_spdx2_file for f in files )
9781
9882
83+ @register .filter
84+ def has_md5 (files ):
85+ return any (f .md5_sum for f in files )
86+
87+
88+ @register .filter
89+ def has_sha256 (files ):
90+ return any (f .sha256_sum for f in files )
91+
92+
9993@register .filter
10094def sort_windows (files ):
10195 if not files :
@@ -128,3 +122,73 @@ def sort_windows(files):
128122 other_files .append (file )
129123
130124 return other_files + windows_files
125+
126+
127+ def get_release_cycle_data () -> dict | None :
128+ """Fetch and cache the release cycle data from PEPs API."""
129+ data = cache .get (RELEASE_CYCLE_CACHE_KEY )
130+ if data is not None :
131+ return data
132+
133+ try :
134+ response = requests .get (RELEASE_CYCLE_URL , timeout = 5 )
135+ response .raise_for_status ()
136+ data = response .json ()
137+ cache .set (RELEASE_CYCLE_CACHE_KEY , data , RELEASE_CYCLE_CACHE_TIMEOUT )
138+ return data
139+ except (requests .RequestException , ValueError ) as e :
140+ logger .warning ("Failed to fetch release cycle data: %s" , e )
141+ return None
142+
143+
144+ @register .inclusion_tag ("downloads/active-releases.html" )
145+ def render_active_releases ():
146+ """Render the active Python releases table from PEPs API data."""
147+ releases = []
148+ release_cycle = get_release_cycle_data ()
149+
150+ if release_cycle :
151+ # Sort releases in descending order (newest first)
152+ sorted_releases = sorted (
153+ release_cycle .keys (),
154+ key = lambda v : [int (x ) for x in v .split ("." )],
155+ reverse = True ,
156+ )
157+
158+ found_eol = False
159+ for release in sorted_releases :
160+ info = release_cycle [release ]
161+ status = info .get ("status" , "" )
162+ first_release = info .get ("first_release" , "" )
163+
164+ if status == "feature" and first_release :
165+ first_release = f"{ first_release } (planned)"
166+
167+ if status == "feature" :
168+ status = "pre-release"
169+
170+ if status == "end-of-life" :
171+ # Include only the most recent EOL release
172+ if found_eol :
173+ continue
174+ found_eol = True
175+
176+ # Get last release for EOL versions
177+ minor = int (release .split ("." )[1 ])
178+ last_release = Release .objects .latest_python3 (minor )
179+ if last_release :
180+ status = format_html (
181+ 'end-of-life, last release was <a href="{}">{}</a>' ,
182+ last_release .get_absolute_url (),
183+ last_release .get_version (),
184+ )
185+
186+ releases .append ({
187+ "version" : release ,
188+ "status" : status ,
189+ "first_release" : first_release ,
190+ "end_of_life" : info .get ("end_of_life" , "" ),
191+ "pep" : info .get ("pep" ),
192+ })
193+
194+ return {"releases" : releases }
0 commit comments