|
| 1 | +import logging |
| 2 | +from typing import Any |
| 3 | + |
| 4 | +import requests |
| 5 | +from datetime import datetime |
| 6 | + |
| 7 | +from django.contrib.sites.shortcuts import get_current_site |
| 8 | +from django.core.handlers.wsgi import WSGIRequest |
1 | 9 | from django.db.models import Prefetch
|
2 | 10 | from django.urls import reverse
|
3 | 11 | from django.views.generic import DetailView, TemplateView, ListView, RedirectView
|
4 | 12 | from django.http import Http404
|
| 13 | +from django.contrib.syndication.views import Feed |
| 14 | +from django.utils.feedgenerator import Rss201rev2Feed |
| 15 | +import pytz |
5 | 16 |
|
6 | 17 | from .models import OS, Release, ReleaseFile
|
7 | 18 |
|
| 19 | +logger = logging.getLogger(__name__) |
8 | 20 |
|
9 | 21 | class DownloadLatestPython2(RedirectView):
|
10 | 22 | """ Redirect to latest Python 2 release """
|
@@ -147,3 +159,92 @@ def get_context_data(self, **kwargs):
|
147 | 159 | )
|
148 | 160 |
|
149 | 161 | return context
|
| 162 | + |
| 163 | + |
| 164 | +class ReleaseFeed(Feed): |
| 165 | + """Generate an RSS feed of the latest Python releases. |
| 166 | +
|
| 167 | + .. note:: It may seem like these are unused methods, but the superclass uses them |
| 168 | + using Django's Syndication framework. |
| 169 | + Docs: https://docs.djangoproject.com/en/4.2/ref/contrib/syndication/ |
| 170 | + """ |
| 171 | + |
| 172 | + feed_type = Rss201rev2Feed |
| 173 | + title = "Python Releases" |
| 174 | + description = "Latest Python releases from Python.org" |
| 175 | + |
| 176 | + @staticmethod |
| 177 | + def link() -> str: |
| 178 | + """Return the URL to the main downloads page.""" |
| 179 | + return reverse('downloads:download') |
| 180 | + |
| 181 | + def get_feed(self, obj: Any, request: WSGIRequest) -> Feed: |
| 182 | + """Store the request object for later use.""" |
| 183 | + self.request = request |
| 184 | + return super().get_feed(obj, request) |
| 185 | + |
| 186 | + def items(self) -> list[dict[str, Any]]: |
| 187 | + """Return the latest Python releases.""" |
| 188 | + url = self.create_url("/api/v2/downloads/release/") |
| 189 | + logger.info(f"Fetching releases from: {url}") |
| 190 | + try: |
| 191 | + return self._fetch_releases(url) |
| 192 | + except requests.RequestException as e: |
| 193 | + logger.error(f"Error fetching releases from API: {str(e)}") |
| 194 | + except ValueError as e: |
| 195 | + logger.error(f"Error parsing JSON from API response: {str(e)}") |
| 196 | + except Exception as e: |
| 197 | + logger.error(f"Unexpected error in items method: {str(e)}") |
| 198 | + return [] |
| 199 | + |
| 200 | + @staticmethod |
| 201 | + def _fetch_releases(url: str) -> list[dict[str, Any]]: |
| 202 | + """Grabs the latest Python releases from API. |
| 203 | +
|
| 204 | +
|
| 205 | + """ |
| 206 | + response = requests.get(url, timeout=10) |
| 207 | + response.raise_for_status() |
| 208 | + data = response.json() |
| 209 | + |
| 210 | + sorted_releases = sorted(data, key=lambda x: x["release_date"], reverse=True) |
| 211 | + return sorted_releases[:10] |
| 212 | + |
| 213 | + def item_title(self, item: dict[str, Any]) -> str: |
| 214 | + """Return the release name as the item title.""" |
| 215 | + return item.get("name", "Unknown Release") |
| 216 | + |
| 217 | + def item_description(self, item: dict[str, Any]) -> str: |
| 218 | + """Return the release version and release date as the item description.""" |
| 219 | + version = item.get("version", "Unknown") |
| 220 | + release_date = item.get("release_date", "Unknown") |
| 221 | + return f"Version: {version}, Release Date: {release_date}" |
| 222 | + |
| 223 | + def item_link(self, item: dict[str, Any]) -> str: |
| 224 | + """Return the URL to the release page on python.org.""" |
| 225 | + return reverse("downloads:download_release_detail", args=[item.get("slug", "")]) |
| 226 | + |
| 227 | + @staticmethod |
| 228 | + def item_pubdate(item: dict[str, Any]) -> datetime: |
| 229 | + """Return the release date as the item publication date.""" |
| 230 | + try: |
| 231 | + release_date = datetime.strptime( |
| 232 | + item.get("release_date", ""), "%Y-%m-%dT%H:%M:%SZ" |
| 233 | + ) |
| 234 | + return pytz.utc.localize(release_date) |
| 235 | + except ValueError: |
| 236 | + logger.error( |
| 237 | + f"Invalid release date format for item: {item.get('name', 'Unknown')}" |
| 238 | + ) |
| 239 | + return pytz.utc.localize(datetime.now()) |
| 240 | + |
| 241 | + @staticmethod |
| 242 | + def item_guid(item: dict[str, Any]) -> str: |
| 243 | + """Return the release URI as the item GUID.""" |
| 244 | + return item.get("resource_uri", "") |
| 245 | + |
| 246 | + def create_url(self, path: str) -> str: |
| 247 | + """Create a full URL using the current site domain.""" |
| 248 | + current_site = get_current_site(self.request) |
| 249 | + scheme = "https" if self.request.is_secure() else "http" |
| 250 | + return f"{scheme}://{current_site.domain}{path}" |
0 commit comments