Coverage for src/gitlabracadabra/packages/github.py: 86%
79 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-14 23:10 +0200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-14 23:10 +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/>.
17from __future__ import annotations
19from logging import getLogger
20from os import getenv
21from typing import TYPE_CHECKING
23from github import Github as PyGithub
24from github.GithubException import UnknownObjectException
26from gitlabracadabra.matchers import Matcher
27from gitlabracadabra.packages.package_file import PackageFile
28from gitlabracadabra.packages.source import Source
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
33logger = getLogger(__name__)
36class Github(Source):
37 """Github source."""
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.
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 []
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
82 def __str__(self) -> str:
83 """Return string representation.
85 Returns:
86 A string.
87 """
88 return f"Github repository (full_name={self._full_name})"
90 @property
91 def package_files(self) -> list[PackageFile]:
92 """Return list of package files.
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
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
127 def _get_all_tag_names(self) -> list[str]:
128 return list(self._get_all_releases().keys())
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
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
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 )