Skip to content

repository

appimage_updater.repositories.sourceforge.repository

SourceForge repository implementation for AppImage Updater.

This module provides support for downloading AppImages from SourceForge projects. SourceForge URLs typically follow the pattern: https://sourceforge.net/projects/{project}/files/{path}/

SourceForgeRepository(timeout=30, user_agent=None, **kwargs)

SourceForge repository implementation.

Parameters:

Name Type Description Default
timeout int

Request timeout in seconds

30
user_agent str | None

Custom user agent string

None
**kwargs Any

Additional configuration options

{}
Source code in src/appimage_updater/repositories/sourceforge/repository.py
def __init__(
    self,
    timeout: int = 30,
    user_agent: str | None = None,
    **kwargs: Any,
) -> None:
    """Initialize SourceForge repository client.

    Args:
        timeout: Request timeout in seconds
        user_agent: Custom user agent string
        **kwargs: Additional configuration options
    """
    super().__init__(timeout=timeout, user_agent=user_agent, **kwargs)
    logger.debug("SourceForge repository client initialized")

detect_repository_type(url)

Check if this client can handle the given repository URL.

Parameters:

Name Type Description Default
url str

Repository URL to check

required

Returns:

Type Description
bool

True if this is a SourceForge URL, False otherwise

Source code in src/appimage_updater/repositories/sourceforge/repository.py
def detect_repository_type(self, url: str) -> bool:
    """Check if this client can handle the given repository URL.

    Args:
        url: Repository URL to check

    Returns:
        True if this is a SourceForge URL, False otherwise
    """
    return "sourceforge.net" in url.lower()

generate_pattern_from_releases(url) async

Generate file pattern from actual releases.

Parameters:

Name Type Description Default
url str

Repository URL to analyze for release patterns

required

Returns:

Type Description
str | None

str | None: Regex pattern string if successful, None otherwise

Source code in src/appimage_updater/repositories/sourceforge/repository.py
async def generate_pattern_from_releases(self, url: str) -> str | None:
    """Generate file pattern from actual releases.

    Args:
        url: Repository URL to analyze for release patterns

    Returns:
        str | None: Regex pattern string if successful, None otherwise
    """
    try:
        releases = await self.get_releases(url, limit=10)

        if not releases:
            logger.debug(f"No releases found for pattern generation: {url}")
            return None

        asset_names = [asset.name for release in releases for asset in release.assets if asset.name]

        if not asset_names:
            logger.debug(f"No suitable assets found for pattern generation: {url}")
            return None

        return self._generate_pattern_from_names(asset_names)

    except Exception as e:
        logger.debug(f"Failed to generate pattern from SourceForge releases: {e}")
        return None

get_latest_release(repo_url) async

Get the latest stable release for a repository.

Parameters:

Name Type Description Default
repo_url str

Repository URL

required

Returns:

Type Description
Release

Release object with release information

Raises:

Type Description
RepositoryError

If the operation fails

Source code in src/appimage_updater/repositories/sourceforge/repository.py
async def get_latest_release(self, repo_url: str) -> Release:
    """Get the latest stable release for a repository.

    Args:
        repo_url: Repository URL

    Returns:
        Release object with release information

    Raises:
        RepositoryError: If the operation fails
    """
    releases = await self.get_releases(repo_url, limit=1)
    if not releases:
        raise RepositoryError(f"No releases found for {repo_url}")
    return releases[0]

get_latest_release_including_prerelease(repo_url) async

Get the latest release including prereleases.

Parameters:

Name Type Description Default
repo_url str

Repository URL

required

Returns:

Type Description
Release

Release object with release information

Raises:

Type Description
RepositoryError

If the operation fails

Source code in src/appimage_updater/repositories/sourceforge/repository.py
async def get_latest_release_including_prerelease(self, repo_url: str) -> Release:
    """Get the latest release including prereleases.

    Args:
        repo_url: Repository URL

    Returns:
        Release object with release information

    Raises:
        RepositoryError: If the operation fails
    """
    # For SourceForge, we just get all releases and return the first one
    # (which may be a prerelease)
    releases = await self.get_releases(repo_url, limit=1)
    if not releases:
        raise RepositoryError(f"No releases found for {repo_url}")
    return releases[0]

get_releases(repo_url, limit=10) async

Get recent releases for a repository.

Parameters:

Name Type Description Default
repo_url str

Repository URL

required
limit int

Maximum number of releases to fetch

10

Returns:

Type Description
list[Release]

List of Release objects

Raises:

Type Description
RepositoryError

If the operation fails

Source code in src/appimage_updater/repositories/sourceforge/repository.py
async def get_releases(self, repo_url: str, limit: int = 10) -> list[Release]:
    """Get recent releases for a repository.

    Args:
        repo_url: Repository URL
        limit: Maximum number of releases to fetch

    Returns:
        List of Release objects

    Raises:
        RepositoryError: If the operation fails
    """
    try:
        project, file_path = self.parse_repo_url(repo_url)
        return await self._fetch_sourceforge_releases(repo_url, project, file_path, limit)

    except (httpx.HTTPError, httpx.TimeoutException, OSError) as e:
        logger.error(f"Failed to get releases for {repo_url}: {e}")
        raise RepositoryError(f"Failed to fetch release information: {e}") from e

normalize_repo_url(url)

Normalize repository URL and detect if it was corrected.

Parameters:

Name Type Description Default
url str

Repository URL to normalize

required

Returns:

Type Description
tuple[str, bool]

Tuple of (normalized_url, was_corrected)

Source code in src/appimage_updater/repositories/sourceforge/repository.py
def normalize_repo_url(self, url: str) -> tuple[str, bool]:
    """Normalize repository URL and detect if it was corrected.

    Args:
        url: Repository URL to normalize

    Returns:
        Tuple of (normalized_url, was_corrected)
    """
    original_url = url
    was_corrected = False

    # Remove trailing slashes
    if url.endswith("/"):
        url = url.rstrip("/")
        was_corrected = True

    # Ensure HTTPS protocol
    if url.startswith("http://"):
        url = url.replace("http://", "https://", 1)
        was_corrected = True
    elif not url.startswith("https://"):
        url = f"https://{url}"
        was_corrected = True

    logger.debug(f"Normalized SourceForge URL: {original_url} -> {url} (corrected: {was_corrected})")
    return url, was_corrected

parse_repo_url(url)

Parse repository URL to extract project and path.

Parameters:

Name Type Description Default
url str

Repository URL

required

Returns:

Type Description
tuple[str, str]

Tuple of (project_name, file_path)

Raises:

Type Description
RepositoryError

If URL format is invalid

Source code in src/appimage_updater/repositories/sourceforge/repository.py
def parse_repo_url(self, url: str) -> tuple[str, str]:
    """Parse repository URL to extract project and path.

    Args:
        url: Repository URL

    Returns:
        Tuple of (project_name, file_path)

    Raises:
        RepositoryError: If URL format is invalid
    """
    if not url or not url.strip():
        raise ValueError("URL cannot be empty")

    try:
        # SourceForge URLs: https://sourceforge.net/projects/{project}/files/{path}/
        parsed = urlparse(url)
        path_parts = [part for part in parsed.path.strip("/").split("/") if part]

        return self._parse_repo_url_parts(url, path_parts)

    except Exception as e:
        raise RepositoryError(f"Failed to parse SourceForge URL {url}: {e}") from e

should_enable_prerelease(url) async

Check if prerelease should be automatically enabled for a repository.

Parameters:

Name Type Description Default
url str

Repository URL

required

Returns:

Type Description
bool

True if only prereleases are found, False if stable releases exist

Source code in src/appimage_updater/repositories/sourceforge/repository.py
async def should_enable_prerelease(self, url: str) -> bool:
    """Check if prerelease should be automatically enabled for a repository.

    Args:
        url: Repository URL

    Returns:
        True if only prereleases are found, False if stable releases exist
    """
    try:
        releases = await self.get_releases(url, limit=20)

        if not releases:
            logger.debug(f"No releases found for {url}, prerelease detection inconclusive")
            return False

        return self._should_enable_prerelease(url, releases)

    except Exception as e:
        logger.debug(f"Could not determine prerelease status for {url}: {e}")
        return False