Coverage for src/gitlabracadabra/packages/github.py: 86%

79 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-10 17:02 +0100

1# 

2# Copyright (C) 2019-2025 Mathieu Parent <math.parent@gmail.com> 

3# 

4# This program is free software: you can redistribute it and/or modify 

5# it under the terms of the GNU Lesser General Public License as published by 

6# the Free Software Foundation, either version 3 of the License, or 

7# (at your option) any later version. 

8# 

9# This program is distributed in the hope that it will be useful, 

10# but WITHOUT ANY WARRANTY; without even the implied warranty of 

11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

12# GNU Lesser General Public License for more details. 

13# 

14# You should have received a copy of the GNU Lesser General Public License 

15# along with this program. If not, see <http://www.gnu.org/licenses/>. 

16 

17from __future__ import annotations 

18 

19from logging import getLogger 

20from os import getenv 

21from typing import TYPE_CHECKING 

22 

23from github import Github as PyGithub 

24from github.GithubException import UnknownObjectException 

25 

26from gitlabracadabra.matchers import Matcher 

27from gitlabracadabra.packages.package_file import PackageFile 

28from gitlabracadabra.packages.source import Source 

29 

30if TYPE_CHECKING: 30 ↛ 31line 30 didn't jump to line 31 because the condition on line 30 was never true

31 from github.GitRelease import GitRelease 

32 

33logger = getLogger(__name__) 

34 

35 

36class Github(Source): 

37 """Github source.""" 

38 

39 def __init__( 

40 self, 

41 *, 

42 log_prefix: str = "", 

43 full_name: str, 

44 package_name: str | None = None, 

45 tags: list[str] | None = None, 

46 semver: str | None = None, 

47 latest_release: bool | None = None, 

48 tarball: bool | None = None, 

49 zipball: bool | None = None, 

50 assets: list[str] | None = None, 

51 ) -> None: 

52 """Initialize a Github source. 

53 

54 Args: 

55 log_prefix: Log prefix. 

56 full_name: Repository full name. Mandatory. 

57 package_name: Destination package name (defaults to repository name). 

58 tags: List of tags. 

59 semver: Semantic version applied on tags. 

60 latest_release: Get latest release. 

61 tarball: Get repository tarball (defaults to False). 

62 zipball: Get repository zip (defaults to False). 

63 assets: List of assets (None by default). 

64 """ 

65 super().__init__() 

66 self._log_prefix = log_prefix 

67 self._full_name = full_name 

68 self._package_name = package_name or full_name.split("/").pop() 

69 self._tags = tags or [] 

70 self._semver = semver 

71 self._latest_release = latest_release or False 

72 self._tarball = tarball or False 

73 self._zipball = zipball or False 

74 self._assets = assets or [] 

75 

76 self._repo = PyGithub( 

77 login_or_token=getenv("GITHUB_TOKEN"), 

78 ).get_repo(self._full_name, lazy=True) 

79 self._all_releases: dict[str, GitRelease] | None = None 

80 self._matching_releases: dict[str, GitRelease] | None = None 

81 

82 def __str__(self) -> str: 

83 """Return string representation. 

84 

85 Returns: 

86 A string. 

87 """ 

88 return f"Github repository (full_name={self._full_name})" 

89 

90 @property 

91 def package_files(self) -> list[PackageFile]: 

92 """Return list of package files. 

93 

94 Returns: 

95 List of package files. 

96 """ 

97 package_files: list[PackageFile] = [] 

98 for release in self._get_matching_releases().values(): 

99 self._append_package_file_from_release(package_files, release) 

100 return package_files 

101 

102 def _get_matching_releases(self) -> dict[str, GitRelease]: 

103 if self._matching_releases is None: 103 ↛ 125line 103 didn't jump to line 125 because the condition on line 103 was always true

104 matches = Matcher( 

105 self._tags, 

106 self._semver, 

107 log_prefix=self._log_prefix, 

108 ).match( 

109 self._get_all_tag_names, 

110 ) 

111 self._matching_releases = {} 

112 for match in matches: 

113 self._append_matching_release(match[0]) 

114 if self._latest_release: 114 ↛ 125line 114 didn't jump to line 125 because the condition on line 114 was always true

115 try: 

116 latest_release = self._repo.get_latest_release() 

117 self._matching_releases[latest_release.tag_name] = latest_release 

118 except UnknownObjectException as err2: 

119 logger.warning( 

120 "%sError getting package files from repository %s, latest release: %s", 

121 self._log_prefix, 

122 self._full_name, 

123 str(err2), 

124 ) 

125 return self._matching_releases 

126 

127 def _get_all_tag_names(self) -> list[str]: 

128 return list(self._get_all_releases().keys()) 

129 

130 def _get_all_releases(self) -> dict[str, GitRelease]: 

131 if self._all_releases is None: 131 ↛ 135line 131 didn't jump to line 135 because the condition on line 131 was always true

132 self._all_releases = {} 

133 for release in self._repo.get_releases(): 

134 self._all_releases[release.tag_name] = release 

135 return self._all_releases 

136 

137 def _append_matching_release(self, tag_name: str) -> None: 

138 if self._all_releases is None: 138 ↛ 139line 138 didn't jump to line 139 because the condition on line 138 was never true

139 try: 

140 self._matching_releases[tag_name] = self._repo.get_release(tag_name) # type: ignore 

141 except UnknownObjectException as err: 

142 logger.warning( 

143 "%sError getting package files from repository %s, release with tag %s: %s", 

144 self._log_prefix, 

145 self._full_name, 

146 tag_name, 

147 str(err), 

148 ) 

149 else: 

150 self._matching_releases[tag_name] = self._all_releases[tag_name] # type: ignore 

151 

152 def _append_package_file_from_release(self, package_files: list[PackageFile], release: GitRelease) -> None: 

153 if self._tarball: 

154 package_files.append( 

155 PackageFile( 

156 release.tarball_url, 

157 "raw", 

158 self._package_name, 

159 release.tag_name, 

160 "{}-{}.tar.gz".format(self._full_name.split("/").pop(), release.tag_name), 

161 ) 

162 ) 

163 if self._zipball: 

164 package_files.append( 

165 PackageFile( 

166 release.zipball_url, 

167 "raw", 

168 self._package_name, 

169 release.tag_name, 

170 "{}-{}.zip".format(self._full_name.split("/").pop(), release.tag_name), 

171 ) 

172 ) 

173 if self._assets: 

174 try: 

175 # https://github.com/PyGithub/PyGithub/pull/1899 

176 assets = release.assets 

177 except AttributeError: 

178 assets = list(release.get_assets()) 

179 assets_map = {asset.name: asset.browser_download_url for asset in assets} 

180 for asset_name in self._assets: 

181 try: 

182 package_files.append( 

183 PackageFile( 

184 assets_map[asset_name], 

185 "raw", 

186 self._package_name, 

187 release.tag_name, 

188 asset_name, 

189 ) 

190 ) 

191 except KeyError: 

192 logger.warning( 

193 '%sAsset "%s" not found from repository %s in release with tag %s', 

194 self._log_prefix, 

195 asset_name, 

196 self._full_name, 

197 release.tag_name, 

198 )