44information, extract AppImage assets, and manage GitHub-specific operations.
55"""
66
7+ import re
8+ from dataclasses import dataclass
79from typing import Any , TypedDict , cast
810from urllib .parse import urlparse
911
@@ -28,9 +30,38 @@ class GitHubReleaseDetails(TypedDict):
2830 assets : list [GitHubAsset ]
2931
3032
33+ @dataclass (slots = True , frozen = True )
34+ class ChecksumFileInfo :
35+ """Information about detected checksum file."""
36+
37+ filename : str
38+ url : str
39+ format_type : str # 'yaml' or 'traditional'
40+
41+
3142class GitHubReleaseFetcher :
3243 """Fetches GitHub release information and extracts AppImage assets."""
3344
45+ # Common checksum file patterns to look for in GitHub releases
46+ CHECKSUM_FILE_PATTERNS = [
47+ r"latest-.*\.yml$" ,
48+ r"latest-.*\.yaml$" ,
49+ r".*checksums?\.txt$" ,
50+ r".*checksums?\.yml$" ,
51+ r".*checksums?\.yaml$" ,
52+ r".*checksums?\.md5$" ,
53+ r".*checksums?\.sha1$" ,
54+ r".*checksums?\.sha256$" ,
55+ r".*checksums?\.sha512$" ,
56+ r"SHA\d+SUMS?(\.txt)?$" ,
57+ r"MD5SUMS?(\.txt)?$" ,
58+ r".*\.sum$" ,
59+ r".*\.hash$" ,
60+ r".*\.digest$" ,
61+ r".*appimage\.sha256$" ,
62+ r".*appimage\.sha512$" ,
63+ ]
64+
3465 def __init__ (self , owner : str , repo : str , session : aiohttp .ClientSession ) -> None :
3566 """Initialize the GitHub release fetcher.
3667
@@ -70,6 +101,48 @@ def _normalize_version(self, tag_name: str) -> str:
70101
71102 return normalized
72103
104+ @staticmethod
105+ def detect_checksum_files (
106+ assets : list [GitHubAsset ],
107+ tag_name : str ,
108+ ) -> list [ChecksumFileInfo ]:
109+ """Detect checksum files in GitHub release assets.
110+
111+ Args:
112+ assets: List of GitHub release assets
113+ tag_name: Release tag name
114+
115+ Returns:
116+ List of detected checksum files with their info
117+
118+ """
119+ checksum_files = []
120+
121+ for asset in assets :
122+ asset_name = asset ["name" ]
123+
124+ # Check if asset matches any checksum file pattern
125+ for pattern in GitHubReleaseFetcher .CHECKSUM_FILE_PATTERNS :
126+ if re .search (pattern , asset_name , re .IGNORECASE ):
127+ url = asset ["browser_download_url" ]
128+
129+ # Determine format type
130+ format_type = (
131+ "yaml"
132+ if asset_name .lower ().endswith ((".yml" , ".yaml" ))
133+ else "traditional"
134+ )
135+
136+ checksum_files .append (
137+ ChecksumFileInfo (filename = asset_name , url = url , format_type = format_type )
138+ )
139+ break
140+
141+ # Prioritize YAML files (like latest-linux.yml) first as they're often more reliable
142+ checksum_files .sort (key = lambda x : (x .format_type != "yaml" , x .filename ))
143+
144+ return checksum_files
145+
73146 async def fetch_latest_release (self ) -> GitHubReleaseDetails :
74147 """Fetch the latest release information from GitHub API.
75148
@@ -293,7 +366,7 @@ def extract_appimage_asset(self, release_data: GitHubReleaseDetails) -> GitHubAs
293366
294367 """
295368 for asset in release_data ["assets" ]:
296- if asset ["name" ].endswith (".AppImage" ):
369+ if asset ["name" ].endswith (".AppImage" ) or asset [ "name" ]. endswith ( ".appimage" ) :
297370 return asset
298371 return None
299372
@@ -308,7 +381,9 @@ def extract_appimage_assets(self, release_data: GitHubReleaseDetails) -> list[Gi
308381
309382 """
310383 return [
311- asset for asset in release_data ["assets" ] if asset ["name" ].endswith (".AppImage" )
384+ asset
385+ for asset in release_data ["assets" ]
386+ if asset ["name" ].endswith (".AppImage" ) or asset ["name" ].endswith (".appimage" )
312387 ]
313388
314389 def select_best_appimage (
0 commit comments