Coverage for src/gitlabracadabra/packages/gitlab.py: 90%
62 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-23 06:44 +0200
« 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/>.
17from __future__ import annotations
19from logging import getLogger
20from typing import TYPE_CHECKING
21from urllib.parse import quote
23from gitlab.v4.objects.packages import ProjectPackage, ProjectPackageFile
25from gitlabracadabra.packages.destination import Destination
27if TYPE_CHECKING: 27 ↛ 28line 27 didn't jump to line 28 because the condition on line 27 was never true
28 from gitlabracadabra.gitlab.connection import GitlabConnection
29 from gitlabracadabra.packages.package_file import PackageFile
32HELM = "helm"
33PYPI = "pypi"
35logger = getLogger(__name__)
38class Gitlab(Destination):
39 """Gitlab repository."""
41 def __init__(
42 self,
43 *,
44 connection: GitlabConnection,
45 full_path: str,
46 project_id: int,
47 ) -> None:
48 """Initialize Gitlab repository.
50 Args:
51 connection: A Gitlab connection.
52 full_path: Project full path.
53 project_id: Project ID.
54 """
55 super().__init__(log_prefix=f"[{full_path}] ")
56 self._connection = connection
57 self._full_path = full_path
58 self._project_id = project_id
59 self._connection.session_callback(self.session)
60 # dict[
61 # tuple[package_name: str, package_version: str],
62 # list[ProjectPackageFile]
63 # ]
64 self._project_package_package_files_cache: dict[tuple[str, str], list[ProjectPackageFile]] = {}
66 def upload_method(self, package_file: PackageFile) -> str:
67 """Get upload HTTP method.
69 Args:
70 package_file: Source package file.
72 Returns:
73 The upload method.
74 """
75 if package_file.package_type in {HELM, PYPI}:
76 return "POST"
78 return super().upload_method(package_file)
80 def get_url(self, package_file: PackageFile) -> str:
81 """Get URL to test existence of destination package file with a HEAD request.
83 Args:
84 package_file: Source package file.
86 Returns:
87 An URL.
89 Raises:
90 NotImplementedError: For unsupported package types.
91 """
92 if package_file.package_type == "generic":
93 return "{}/projects/{}/packages/generic/{}/{}/{}".format(
94 self._connection.api_url,
95 quote(self._full_path, safe=""),
96 quote(package_file.package_name, safe=""), # [A-Za-z0-9\.\_\-\+]+
97 quote(package_file.package_version, safe=""), # (\.?[\w\+-]+\.?)+
98 quote(package_file.file_name, safe=""), # [A-Za-z0-9\.\_\-\+]+
99 )
100 if package_file.package_type == HELM:
101 channel = package_file.metadata.get("channel") or "stable"
102 file_name = f"{package_file.package_name}-{package_file.package_version}.tgz"
103 return "{}/projects/{}/packages/helm/{}/charts/{}".format(
104 self._connection.api_url,
105 quote(self._full_path, safe=""),
106 quote(channel, safe=""),
107 quote(file_name, safe=""),
108 )
109 if package_file.package_type == PYPI: 109 ↛ 117line 109 didn't jump to line 117 because the condition on line 109 was always true
110 return "{}/projects/{}/packages/pypi/files/{}/{}".format(
111 self._connection.api_url,
112 self._project_id,
113 quote(package_file.metadata.get("sha256", ""), safe=""),
114 quote(package_file.file_name, safe=""),
115 )
117 raise NotImplementedError
119 def upload_url(self, package_file: PackageFile) -> str:
120 """Get URL to upload to.
122 Args:
123 package_file: Source package file.
125 Returns:
126 The upload URL.
127 """
128 if package_file.package_type == HELM:
129 channel = package_file.metadata.get("channel") or "stable"
130 return "{}/projects/{}/packages/helm/api/{}/charts".format(
131 self._connection.api_url,
132 quote(self._full_path, safe=""),
133 quote(channel, safe=""),
134 )
135 if package_file.package_type == PYPI:
136 return (
137 "{}/projects/{}/packages/pypi?"
138 "requires_python={}&"
139 "name={}&"
140 "version={}&"
141 "md5_digest={}&"
142 "sha256_digest={}"
143 ).format(
144 self._connection.api_url,
145 self._project_id,
146 quote(package_file.metadata.get("requires-python", ""), safe=""),
147 quote(package_file.package_name, safe=""),
148 quote(package_file.package_version, safe=""),
149 quote(package_file.metadata.get("md5", ""), safe=""),
150 quote(package_file.metadata.get("sha256", ""), safe=""),
151 )
153 return super().upload_url(package_file)
155 def cache_project_package_package_files(self, package_type: str, package_name: str, package_version: str) -> None:
156 if (package_name, package_version) not in self._project_package_package_files_cache:
157 self._project_package_package_files_cache[(package_name, package_version)] = (
158 self._project_package_package_files(package_type, package_name, package_version)
159 )
161 def delete_package_file(self, package_file: PackageFile) -> None:
162 # No DELETE endpoint for generic packages
163 # https://gitlab.com/gitlab-org/gitlab/-/issues/536839
164 self.cache_project_package_package_files(
165 package_file.package_type, package_file.package_name, package_file.package_version
166 )
167 for project_package_file in self._project_package_package_files_cache[
168 (package_file.package_name, package_file.package_version)
169 ]:
170 if project_package_file.file_name == package_file.file_name:
171 project_package_file.delete()
173 def files_key(self, package_file: PackageFile) -> str | None:
174 """Get files key, to upload to. If None, uploaded as body.
176 Args:
177 package_file: Source package file.
179 Returns:
180 The files key, or None.
181 """
182 if package_file.package_type == HELM:
183 return "chart"
184 if package_file.package_type == PYPI:
185 return "content"
187 return super().files_key(package_file)
189 def _project_package_package_files(
190 self, package_type: str, package_name: str, package_version: str
191 ) -> list[ProjectPackageFile]:
192 project = self._connection.pygitlab.projects.get(self._full_path, lazy=True)
193 for project_package in project.packages.list( 193 ↛ 207line 193 didn't jump to line 207 because the loop on line 193 didn't complete
194 package_type=package_type,
195 package_name=package_name,
196 package_version=package_version,
197 iterator=True,
198 ):
199 if not isinstance(project_package, ProjectPackage): 199 ↛ 200line 199 didn't jump to line 200 because the condition on line 199 was never true
200 raise TypeError
201 return [
202 project_package_file
203 for project_package_file in project_package.package_files.list(iterator=True)
204 if isinstance(project_package_file, ProjectPackageFile)
205 ]
207 return []