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

79 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-23 06:44 +0200

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 

33 from gitlabracadabra.packages.destination import Destination 

34 

35logger = getLogger(__name__) 

36 

37 

38class Github(Source): 

39 """Github source.""" 

40 

41 def __init__( 

42 self, 

43 *, 

44 log_prefix: str = "", 

45 full_name: str, 

46 package_name: str | None = None, 

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

48 semver: str | None = None, 

49 latest_release: bool | None = None, 

50 tarball: bool | None = None, 

51 zipball: bool | None = None, 

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

53 ) -> None: 

54 """Initialize a Github source. 

55 

56 Args: 

57 log_prefix: Log prefix. 

58 full_name: Repository full name. Mandatory. 

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

60 tags: List of tags. 

61 semver: Semantic version applied on tags. 

62 latest_release: Get latest release. 

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

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

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

66 """ 

67 super().__init__() 

68 self._log_prefix = log_prefix 

69 self._full_name = full_name 

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

71 self._tags = tags or [] 

72 self._semver = semver 

73 self._latest_release = latest_release or False 

74 self._tarball = tarball or False 

75 self._zipball = zipball or False 

76 self._assets = assets or [] 

77 

78 self._repo = PyGithub( 

79 login_or_token=getenv("GITHUB_TOKEN"), 

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

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

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

83 

84 def __str__(self) -> str: 

85 """Return string representation. 

86 

87 Returns: 

88 A string. 

89 """ 

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

91 

92 def package_files( 

93 self, 

94 destination: Destination, # noqa: ARG002 

95 ) -> list[PackageFile]: 

96 """Return list of package files. 

97 

98 Returns: 

99 List of package files. 

100 """ 

101 package_files: list[PackageFile] = [] 

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

103 self._append_package_file_from_release(package_files, release) 

104 return package_files 

105 

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

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

108 matches = Matcher( 

109 self._tags, 

110 self._semver, 

111 log_prefix=self._log_prefix, 

112 ).match( 

113 self._get_all_tag_names, 

114 ) 

115 self._matching_releases = {} 

116 for match in matches: 

117 self._append_matching_release(match[0]) 

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

119 try: 

120 latest_release = self._repo.get_latest_release() 

121 self._matching_releases[latest_release.tag_name] = latest_release 

122 except UnknownObjectException as err2: 

123 logger.warning( 

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

125 self._log_prefix, 

126 self._full_name, 

127 str(err2), 

128 ) 

129 return self._matching_releases 

130 

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

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

133 

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

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

136 self._all_releases = {} 

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

138 self._all_releases[release.tag_name] = release 

139 return self._all_releases 

140 

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

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

143 try: 

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

145 except UnknownObjectException as err: 

146 logger.warning( 

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

148 self._log_prefix, 

149 self._full_name, 

150 tag_name, 

151 str(err), 

152 ) 

153 else: 

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

155 

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

157 if self._tarball: 

158 package_files.append( 

159 PackageFile( 

160 release.tarball_url, 

161 "generic", 

162 self._package_name, 

163 release.tag_name, 

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

165 ) 

166 ) 

167 if self._zipball: 

168 package_files.append( 

169 PackageFile( 

170 release.zipball_url, 

171 "generic", 

172 self._package_name, 

173 release.tag_name, 

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

175 ) 

176 ) 

177 if self._assets: 

178 try: 

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

180 assets = release.assets 

181 except AttributeError: 

182 assets = list(release.get_assets()) 

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

184 for asset_name in self._assets: 

185 try: 

186 package_files.append( 

187 PackageFile( 

188 assets_map[asset_name], 

189 "generic", 

190 self._package_name, 

191 release.tag_name, 

192 asset_name, 

193 ) 

194 ) 

195 except KeyError: 

196 logger.warning( 

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

198 self._log_prefix, 

199 asset_name, 

200 self._full_name, 

201 release.tag_name, 

202 )