Source code for pydotorg.domains.downloads.models

"""Downloads domain models."""

from __future__ import annotations

import datetime
from enum import StrEnum
from typing import TYPE_CHECKING
from uuid import UUID

from sqlalchemy import BigInteger, Boolean, Date, Enum, ForeignKey, Index, Integer, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship

from pydotorg.core.database.base import AuditBase, ContentManageableMixin, NameSlugMixin, UUIDAuditBase

if TYPE_CHECKING:
    from pydotorg.domains.pages.models import Page


[docs] class PythonVersion(StrEnum): PYTHON1 = "1" PYTHON2 = "2" PYTHON3 = "3" PYMANAGER = "manager"
[docs] class ReleaseStatus(StrEnum): PRERELEASE = "prerelease" BUGFIX = "bugfix" SECURITY = "security" EOL = "eol"
[docs] class OS(AuditBase, ContentManageableMixin, NameSlugMixin): __tablename__ = "download_os" releases: Mapped[list[ReleaseFile]] = relationship( "ReleaseFile", back_populates="os", lazy="noload", )
[docs] class Release(AuditBase, ContentManageableMixin, NameSlugMixin): __tablename__ = "releases" version: Mapped[PythonVersion] = mapped_column( Enum(PythonVersion, values_callable=lambda x: [e.value for e in x]), default=PythonVersion.PYTHON3, ) status: Mapped[ReleaseStatus] = mapped_column( Enum(ReleaseStatus, values_callable=lambda x: [e.value for e in x]), default=ReleaseStatus.BUGFIX, ) is_latest: Mapped[bool] = mapped_column(Boolean, default=False, index=True) is_published: Mapped[bool] = mapped_column(Boolean, default=False, index=True) pre_release: Mapped[bool] = mapped_column(Boolean, default=False) show_on_download_page: Mapped[bool] = mapped_column(Boolean, default=True) release_date: Mapped[datetime.date | None] = mapped_column(Date, nullable=True) eol_date: Mapped[datetime.date | None] = mapped_column(Date, nullable=True) release_page_id: Mapped[UUID | None] = mapped_column( ForeignKey("pages.id", ondelete="SET NULL"), nullable=True, ) release_notes_url: Mapped[str] = mapped_column(String(500), default="") content: Mapped[str] = mapped_column(Text, default="") release_page: Mapped[Page | None] = relationship("Page", lazy="selectin") files: Mapped[list[ReleaseFile]] = relationship( "ReleaseFile", back_populates="release", cascade="all, delete-orphan", lazy="selectin", ) def is_version_at_least(self, min_version: tuple[int, ...]) -> bool: try: parts = self.name.split(".") version_tuple = tuple(int(p) for p in parts[:2] if p.isdigit()) except (ValueError, IndexError): return False else: return version_tuple >= min_version @property def is_version_at_least_3_5(self) -> bool: return self.is_version_at_least((3, 5)) @property def is_version_at_least_3_9(self) -> bool: return self.is_version_at_least((3, 9)) @property def is_version_at_least_3_14(self) -> bool: return self.is_version_at_least((3, 14)) @property def minor_version(self) -> str: """Extract minor version string (e.g., '3.12' from '3.12.1'). Returns: Minor version string or full name if parsing fails. """ try: parts = self.name.split(".") if len(parts) >= 2: major = parts[0].lstrip("Python ").strip() minor = parts[1] if major.isdigit() and minor.isdigit(): return f"{major}.{minor}" except (ValueError, IndexError): pass # Fall through to return full name as fallback return self.name @property def major_version(self) -> str: """Extract major version string (e.g., '3' from '3.12.1'). Returns: Major version string or full name if parsing fails. """ try: parts = self.name.split(".") if len(parts) >= 1: major = parts[0].lstrip("Python ").strip() if major.isdigit(): return major except (ValueError, IndexError): pass # Fall through to return full name as fallback return self.name @property def is_eol(self) -> bool: """Check if this release series has reached end of life. Returns: True if status is EOL or eol_date has passed. """ if self.status == ReleaseStatus.EOL: return True return bool(self.eol_date and self.eol_date < datetime.datetime.now(tz=datetime.UTC).date()) @property def is_prerelease(self) -> bool: """Check if this is a pre-release version (alpha, beta, RC). Returns: True if status is prerelease or pre_release flag is set. """ return self.pre_release or self.status == ReleaseStatus.PRERELEASE @property def status_label(self) -> str: """Get human-readable status label. Returns: Status label string. """ if self.is_eol: return "End of Life" if self.is_prerelease: return "Pre-release" if self.status == ReleaseStatus.SECURITY: return "Security" if self.status == ReleaseStatus.BUGFIX: return "Active" return self.status.value.title() def files_for_os(self, os_slug: str) -> list[ReleaseFile]: return [f for f in self.files if f.os.slug == os_slug] def download_file_for_os(self, os_slug: str) -> ReleaseFile | None: for f in self.files: if f.os.slug == os_slug and f.download_button: return f return None
[docs] class ReleaseFile(AuditBase, ContentManageableMixin, NameSlugMixin): __tablename__ = "release_files" release_id: Mapped[UUID] = mapped_column(ForeignKey("releases.id", ondelete="CASCADE")) os_id: Mapped[UUID] = mapped_column(ForeignKey("download_os.id", ondelete="CASCADE")) description: Mapped[str] = mapped_column(Text, default="") is_source: Mapped[bool] = mapped_column(Boolean, default=False) url: Mapped[str] = mapped_column(String(500)) gpg_signature_file: Mapped[str] = mapped_column(String(500), default="") sigstore_signature_file: Mapped[str] = mapped_column(String(500), default="") sigstore_cert_file: Mapped[str] = mapped_column(String(500), default="") sigstore_bundle_file: Mapped[str] = mapped_column(String(500), default="") sbom_spdx2_file: Mapped[str] = mapped_column(String(500), default="") md5_sum: Mapped[str] = mapped_column(String(200), default="") sha256_sum: Mapped[str] = mapped_column(String(200), default="") filesize: Mapped[int] = mapped_column(BigInteger, default=0) download_button: Mapped[bool] = mapped_column(Boolean, default=False) release: Mapped[Release] = relationship("Release", back_populates="files") os: Mapped[OS] = relationship("OS", back_populates="releases", lazy="selectin") statistics: Mapped[list[DownloadStatistic]] = relationship( "DownloadStatistic", back_populates="release_file", cascade="all, delete-orphan", lazy="noload", )
[docs] class DownloadStatistic(UUIDAuditBase): """Daily download statistics for release files. Stores aggregated download counts per file per day for analytics. Redis provides real-time counters; this table provides historical data. """ __tablename__ = "download_statistics" __table_args__ = ( UniqueConstraint( "release_file_id", "date", name="uq_download_stats_file_date", ), Index("ix_download_stats_date", "date"), Index("ix_download_stats_file_id", "release_file_id"), ) release_file_id: Mapped[UUID] = mapped_column( ForeignKey("release_files.id", ondelete="CASCADE"), nullable=False, ) date: Mapped[datetime.date] = mapped_column(Date, nullable=False) download_count: Mapped[int] = mapped_column(Integer, default=0) release_file: Mapped[ReleaseFile] = relationship( "ReleaseFile", back_populates="statistics", )