Skip to content

pattern_generator

appimage_updater.pattern_generator

Pattern generation and URL handling for AppImage files.

This module contains functions for parsing GitHub URLs, normalizing repository URLs, generating regex patterns for AppImage file matching, and detecting prerelease-only repositories.

create_pattern_from_filenames(filenames, include_both_formats=False)

Create a regex pattern from actual AppImage/ZIP filenames.

Source code in src/appimage_updater/pattern_generator.py
def create_pattern_from_filenames(filenames: list[str], include_both_formats: bool = False) -> str:
    """Create a regex pattern from actual AppImage/ZIP filenames."""
    if not filenames:
        return _build_pattern("", include_both_formats, empty_ok=True)

    base_filenames = _strip_extensions_list(filenames)
    common_prefix = _derive_common_prefix(base_filenames, filenames)
    common_prefix = _generalize_pattern_prefix(common_prefix)
    pattern = _build_pattern(common_prefix, include_both_formats)

    fmt = "both ZIP and AppImage" if include_both_formats else "AppImage"
    logger.debug(f"Created {fmt} pattern '{pattern}' from {len(filenames)} files: {filenames[:3]}...")
    return pattern

detect_source_type(url)

Detect the source type based on the URL.

Source code in src/appimage_updater/pattern_generator.py
def detect_source_type(url: str) -> str:
    """Detect the source type based on the URL."""
    return detect_repository_type(url)

fetch_appimage_pattern_from_github(url) async

Async function to fetch AppImage pattern from repository releases.

Looks for both direct AppImage files and ZIP files that might contain AppImages. Prioritizes stable releases over prereleases for better pattern generation.

Source code in src/appimage_updater/pattern_generator.py
async def fetch_appimage_pattern_from_github(url: str) -> str | None:
    """Async function to fetch AppImage pattern from repository releases.

    Looks for both direct AppImage files and ZIP files that might contain AppImages.
    Prioritizes stable releases over prereleases for better pattern generation.
    """
    try:
        client = get_repository_client(url)
        releases = await client.get_releases(url, limit=20)
        groups = _collect_release_files(releases)
        target_files = _select_target_files(groups)
        if not target_files:
            logger.debug("No AppImage or ZIP files found in any releases")
            return None
        return create_pattern_from_filenames(target_files, include_both_formats=True)
    except Exception as e:
        logger.debug(f"Error fetching releases: {e}")
        return None

find_common_prefix(strings)

Find the longest common prefix among a list of strings.

Source code in src/appimage_updater/pattern_generator.py
def find_common_prefix(strings: list[str]) -> str:
    """Find the longest common prefix among a list of strings."""
    if not strings:
        return ""

    # Start with the first string
    prefix = strings[0]

    for string in strings[1:]:
        # Find common prefix with current string
        common_len = _find_common_length(prefix, string)
        prefix = prefix[:common_len]

        # If prefix becomes too short, stop
        if len(prefix) < 2:
            break

    return prefix

generate_appimage_pattern_async(app_name, url) async

Async version of pattern generation for use in async contexts.

First attempts to fetch actual AppImage files from GitHub releases to create an accurate pattern. Falls back to intelligent defaults if that fails.

Source code in src/appimage_updater/pattern_generator.py
async def generate_appimage_pattern_async(app_name: str, url: str) -> str:
    """Async version of pattern generation for use in async contexts.

    First attempts to fetch actual AppImage files from GitHub releases to create
    an accurate pattern. Falls back to intelligent defaults if that fails.
    """
    try:
        # Try to get pattern from actual GitHub releases
        pattern = await fetch_appimage_pattern_from_github(url)
        if pattern:
            logger.debug(f"Generated pattern from releases: {pattern}")
            return pattern
    except Exception as e:
        logger.debug(f"Failed to generate pattern from releases: {e}")
        # Fall through to fallback logic

    # Fallback: Use intelligent defaults based on the app name and URL
    logger.debug("Using fallback pattern generation")
    return generate_fallback_pattern(app_name, url)

generate_fallback_pattern(app_name, url)

Generate a fallback pattern using app name and URL heuristics.

This is the original logic, kept as a fallback when we can't fetch actual release data from GitHub. Now includes both ZIP and AppImage formats to handle projects that package AppImages inside ZIP files.

Source code in src/appimage_updater/pattern_generator.py
def generate_fallback_pattern(app_name: str, url: str) -> str:
    """Generate a fallback pattern using app name and URL heuristics.

    This is the original logic, kept as a fallback when we can't fetch
    actual release data from GitHub. Now includes both ZIP and AppImage formats
    to handle projects that package AppImages inside ZIP files.
    """
    # Start with the app name as base (prefer app name over repo name for better matching)
    base_name = re.escape(app_name)

    # Check if it's a GitHub URL - but prioritize app name since it's usually more accurate
    github_info = parse_github_url(url)
    if github_info:
        owner, repo = github_info
        # Only use repo name if app_name seems generic or is very different
        # This prevents issues like "desktop" matching "GitHubDesktop"
        if (
            app_name.lower() in ["app", "application", "tool"]  # Generic app names
            or (len(repo) > len(app_name) and app_name.lower() in repo.lower())  # App name is subset of repo
        ):
            base_name = re.escape(repo)

    # Create a flexible pattern that handles common naming conventions
    # Support both ZIP and AppImage formats to handle projects that package AppImages in ZIP files
    # Make pattern flexible for common character substitutions (underscore/hyphen, etc.)
    # Replace both underscores and hyphens with character class allowing either
    flexible_name = re.sub(r"[_-]", "[_-]", base_name)
    pattern = f"(?i){flexible_name}.*\\.(?:zip|AppImage)(\\.(|current|old))?$"

    return pattern

normalize_github_url(url)

Normalize GitHub URL to repository format and detect if it was corrected.

Detects GitHub download URLs (releases/download/...) and converts them to repository URLs. Returns (normalized_url, was_corrected) tuple.

Source code in src/appimage_updater/pattern_generator.py
def normalize_github_url(url: str) -> tuple[str, bool]:
    """Normalize GitHub URL to repository format and detect if it was corrected.

    Detects GitHub download URLs (releases/download/...) and converts them to repository URLs.
    Returns (normalized_url, was_corrected) tuple.
    """
    try:
        if not _is_github_url(url):
            return url, False

        path_parts = _extract_url_path_parts(url)
        if len(path_parts) < 2:
            return url, False

        owner, repo = path_parts[0], path_parts[1]
        return _normalize_github_path(path_parts, owner, repo, url)

    except Exception as e:
        logger.debug(f"Failed to normalize GitHub URL {url}: {e}")
        return url, False

parse_github_url(url)

Parse GitHub URL and extract owner/repo information.

Returns (owner, repo) tuple or None if not a GitHub URL.

Source code in src/appimage_updater/pattern_generator.py
def parse_github_url(url: str) -> tuple[str, str] | None:
    """Parse GitHub URL and extract owner/repo information.

    Returns (owner, repo) tuple or None if not a GitHub URL.
    """
    try:
        parsed = urllib.parse.urlparse(url)
        if parsed.netloc.lower() not in ("github.com", "www.github.com"):
            logger.debug(f"URL {url} is not a GitHub repository URL (netloc: {parsed.netloc})")
            return None

        path_parts = parsed.path.strip("/").split("/")
        if len(path_parts) >= 2:
            return (path_parts[0], path_parts[1])
        logger.debug(f"URL {url} does not have enough path components for owner/repo")
    except Exception as e:
        logger.debug(f"Failed to parse URL {url}: {e}")
    return None

should_enable_prerelease(url) async

Check if prerelease should be automatically enabled for a repository.

Returns True if the repository only has prerelease versions (like continuous builds) and no stable releases, indicating that prerelease support should be enabled.

Parameters:

Name Type Description Default
url str

Repository URL

required

Returns:

Name Type Description
bool bool

True if only prereleases are found, False if stable releases exist or on error

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

    Returns True if the repository only has prerelease versions (like continuous builds)
    and no stable releases, indicating that prerelease support should be enabled.

    Args:
        url: Repository URL

    Returns:
        bool: True if only prereleases are found, False if stable releases exist or on error
    """
    try:
        releases = await _fetch_releases_for_prerelease_check(url)
        if not releases:
            return False

        valid_releases = _filter_valid_releases(releases, url)
        if not valid_releases:
            return False

        return _analyze_prerelease_status(valid_releases, url)

    except Exception as e:
        # Don't fail the add command if prerelease detection fails
        logger.debug(f"Failed to check prerelease status for {url}: {e}")
        return False