Files
shared_libs/gitea.py

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