#!/usr/bin/python3 """ A streamlined module to check for updates from a Gitea repository API. """ import re import requests from typing import Optional, Tuple class GiteaUpdaterError(Exception): """Base exception for GiteaUpdater.""" pass class GiteaApiUrlError(GiteaUpdaterError): """Raised when the Gitea API URL is invalid.""" pass class GiteaVersionParseError(GiteaUpdaterError, ValueError): """Raised when a version string cannot be parsed.""" pass class GiteaApiResponseError(GiteaUpdaterError): """Raised for invalid or unexpected API responses.""" pass class GiteaUpdater: """ Provides a clean interface to check for software updates via a Gitea API. """ @staticmethod def _parse_version(version_string: str) -> Optional[Tuple[int, ...]]: """ Parses a version string into a tuple of integers for comparison. Handles prefixes like 'v. ' or 'v'. It prioritizes parsing as a date-based version string with the format .. (e.g., "v. 2.08.1025"). If the string does not match this format, it falls back to a general semantic versioning parsing (e.g., "v2.1.0"). Args: version_string: The version string (e.g., "v. 1.08.1325", "v2.1.0"). Returns: A tuple of integers for comparison. For date-based versions, the format is (major, year, month, day) to ensure correct comparison. Returns None if parsing fails. """ try: # Remove common prefixes like 'v', 'v. ', etc. cleaned_string = re.sub(r'^[vV\.\s]+', '', version_string) parts = cleaned_string.split('.') # Try to parse as date-based version first if len(parts) == 3: day_year_str = parts[2] if len(day_year_str) >= 3 and day_year_str.isdigit(): day_year_int = int(day_year_str) major = int(parts[0]) month = int(parts[1]) day = day_year_int // 100 year = day_year_int % 100 + 2000 # Basic validation for date components if 1 <= month <= 12 and 1 <= day <= 31: return (major, year, month, day) # Fallback to standard version parsing for other formats (e.g., 2.1.0) return tuple(map(int, parts)) except (ValueError, TypeError): return None @staticmethod def check_for_update(api_url: str, current_version: str) -> Optional[str]: """ Checks for a newer version of the application on Gitea. Args: api_url: The Gitea API URL for releases. current_version: The current version string of the application. Returns: The new version string if an update is available, otherwise None. Raises: GiteaApiUrlError: If the API URL is not provided. GiteaVersionParseError: If the local or remote version string cannot be parsed. GiteaApiResponseError: If the API response is invalid. requests.exceptions.RequestException: For network or HTTP errors. """ if not api_url: raise GiteaApiUrlError("Gitea API URL is not provided.") local_version_tuple = GiteaUpdater._parse_version(current_version) if not local_version_tuple: raise GiteaVersionParseError( f"Could not parse local version string: {current_version}") response = requests.get(api_url, timeout=10) response.raise_for_status() # Raises HTTPError for 4xx/5xx try: data = response.json() if not data: return None # No releases found is not an error latest_tag_name = data[0].get("tag_name") if not latest_tag_name: raise GiteaApiResponseError( "Invalid API response: 'tag_name' not found in the first release.") except (ValueError, IndexError, KeyError) as e: raise GiteaApiResponseError( f"Could not process the response from Gitea: {e}") from e remote_version_tuple = GiteaUpdater._parse_version(latest_tag_name) if not remote_version_tuple: raise GiteaVersionParseError( f"Could not parse remote version string: {latest_tag_name}") if remote_version_tuple > local_version_tuple: return latest_tag_name return None