132 lines
4.5 KiB
Python
132 lines
4.5 KiB
Python
#!/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
|
|
<major>.<month>.<day><year_short> (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
|