glpkg 1.4.2__py3-none-any.whl → 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gitlab/__init__.py CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  from gitlab.packages import Packages
4
4
 
5
- __version__ = "1.4.2"
5
+ __version__ = "1.5.0"
gitlab/cli_handler.py CHANGED
@@ -3,6 +3,7 @@
3
3
  import argparse
4
4
  from netrc import netrc
5
5
  import os
6
+ import re
6
7
  import sys
7
8
  import urllib
8
9
  from gitlab import Packages, __version__
@@ -72,6 +73,9 @@ class CLIHandler:
72
73
  file=sys.stderr,
73
74
  )
74
75
  print("Check your arguments and credentials.", file=sys.stderr)
76
+ except ValueError as e:
77
+ print("Whopsie! Something did go wrong.", file=sys.stderr)
78
+ print(e, file=sys.stderr)
75
79
  return ret
76
80
 
77
81
  def _register_common_arguments(self, parser: argparse.ArgumentParser) -> None:
@@ -89,7 +93,7 @@ class CLIHandler:
89
93
  parser:
90
94
  The argparser where to register the common arguments.
91
95
  """
92
- group = parser.add_mutually_exclusive_group()
96
+ group = parser.add_argument_group()
93
97
  group.add_argument(
94
98
  "-H",
95
99
  "--host",
@@ -148,6 +152,12 @@ class CLIHandler:
148
152
  """
149
153
  self._register_common_arguments(parser)
150
154
  parser.add_argument("-v", "--version", type=str, help="The package version.")
155
+ parser.add_argument(
156
+ "--from-file",
157
+ type=str,
158
+ help="Download all files from packages listed in the file. Each line in the "
159
+ "file should follow format <package-name>==<package-version>@<project>.",
160
+ )
151
161
  parser.add_argument(
152
162
  "-f",
153
163
  "--file",
@@ -195,6 +205,8 @@ class CLIHandler:
195
205
  project = os.environ["CI_PROJECT_ID"]
196
206
  token = os.environ["CI_JOB_TOKEN"]
197
207
  token_user = "JOB-TOKEN"
208
+ if args.host:
209
+ host = args.host
198
210
  if args.project:
199
211
  project = args.project
200
212
  if args.token:
@@ -229,23 +241,67 @@ class CLIHandler:
229
241
  host, project, name, token_user, token = self._args(args)
230
242
  version = args.version
231
243
  destination = args.destination
244
+ file = args.file
245
+ from_file = args.from_file
232
246
  packages = Packages(host, token_user, token)
233
- package_id = packages.get_id(project, name, version)
234
- if package_id:
235
- files = []
236
- if args.file:
237
- files.append(args.file)
238
- else:
239
- files = packages.get_files(project, package_id).keys()
240
- for file in files:
241
- ret = packages.download_file(project, name, version, file, destination)
247
+ if from_file:
248
+ package_list = self._read_from_file(from_file)
249
+ for package in package_list:
250
+ ret = packages.download(
251
+ package["project"], package["name"], package["version"], destination
252
+ )
242
253
  if ret:
243
- print("Failed to download file " + file)
244
254
  break
255
+ elif file:
256
+ ret = packages.download_file(project, name, version, file, destination)
245
257
  else:
246
- print("No package " + name + " version " + version + " found!")
258
+ ret = packages.download(project, name, version, destination)
259
+ if ret:
260
+ if ret == 2:
261
+ print("No package " + name + " version " + version + " found!")
262
+ else:
263
+ print("Failed to download file(s)!")
247
264
  return ret
248
265
 
266
+ def _read_from_file(self, filepath: str) -> list[dict]:
267
+ """
268
+ Reads packages file and returns a list of packages to be downloaded.
269
+
270
+ The lines in the file should be
271
+ <packagename>==<version>@<project>
272
+
273
+ Parameters
274
+ ----------
275
+ filepath : str
276
+ Filepath of the packages file
277
+
278
+ Returns
279
+ -------
280
+ int
281
+ Zero if everything goes well, non-zero otherwise
282
+ """
283
+ packages = []
284
+ pattern = (
285
+ r"\s*(?P<packagename>\S+)\s*=="
286
+ r"\s*(?P<packageversion>\S+)\s*@"
287
+ r"\s*(?P<project>\S+)\s*"
288
+ )
289
+ with open(filepath, "r", encoding="utf-8") as file:
290
+ for line in file:
291
+ if not line:
292
+ continue
293
+ match = re.match(pattern, line)
294
+ if not match:
295
+ raise ValueError(
296
+ f"Package file {filepath} line {line} seems fishy! \
297
+ Check that the line is <packagename>==<version>@<project>"
298
+ )
299
+ name = match.group("packagename")
300
+ version = match.group("packageversion")
301
+ project = match.group("project")
302
+ packages.append({"name": name, "version": version, "project": project})
303
+ return packages
304
+
249
305
  def _register_list_parser(self, parser: argparse.ArgumentParser):
250
306
  """
251
307
  Registers the list command related arguments to the parser:
@@ -334,19 +390,21 @@ class CLIHandler:
334
390
  int
335
391
  Zero if everything went fine, non-zero otherwise.
336
392
  """
337
- ret = 0
393
+ ret = 1
338
394
  host, project, name, token_user, token = self._args(args)
339
395
  version = args.version
340
396
  file = args.file
341
397
  source = args.source
398
+ packages = Packages(host, token_user, token)
342
399
  if file:
343
400
  # Check if the uploaded file exists.
344
401
  if not os.path.isfile(os.path.join(source, file)):
345
402
  print("File " + file + " does not exist!")
346
403
  ret = 1
347
- if not ret:
348
- packages = Packages(host, token_user, token)
349
- ret = packages.upload_file(project, name, version, file, source)
404
+ else:
405
+ ret = packages.upload_file(project, name, version, file, source)
406
+ else:
407
+ ret = packages.upload(project, name, version, source)
350
408
  return ret
351
409
 
352
410
  def _register_delete_parser(self, parser: argparse.ArgumentParser):
@@ -397,5 +455,5 @@ class CLIHandler:
397
455
  if file:
398
456
  ret = packages.delete_file(project, name, version, file)
399
457
  else:
400
- ret = packages.delete_package(project, name, version)
458
+ ret = packages.delete(project, name, version)
401
459
  return ret
@@ -0,0 +1,248 @@
1
+ """GitLab basic API"""
2
+
3
+ from http.client import HTTPMessage
4
+ from json import loads
5
+ import logging
6
+ from urllib import parse
7
+ from gitlab.lib import http
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class GitLabAPI:
13
+ """Class for basic GitLab API"""
14
+
15
+ def __init__(self, host: str, token_type: str, token: str):
16
+ """
17
+ Creates a new instance of class.
18
+
19
+ Parameters
20
+ ----------
21
+ host : str
22
+ The GitLab instance hostname, without schema.
23
+ The host will be used for the package API interaction.
24
+ For example gitlab.com.
25
+ token_type : str
26
+ The token type or "user" to authenticate with GitLab REST API.
27
+ For personal, project, and group tokens this is `PRIVATE-TOKEN`.
28
+ For `CI_JOB_TOKEN` this is `JOB-TOKEN`.
29
+ Can be left empty when authentication is not used.
30
+ token : str
31
+ The token (secret) to authenticate with GitLab REST API.
32
+ This can be a personal token, project token, or`CI_JOB_TOKEN`.
33
+ Leave empty when authentication is not used.
34
+ """
35
+ self.host = host
36
+ self.token_type = token_type
37
+ self.token = token
38
+
39
+ def _url(self) -> str:
40
+ """
41
+ Returns the GitLab REST API URL by using the host variable.
42
+
43
+ Returns
44
+ -------
45
+ str
46
+ The GitLab REST API URL, for example `https://gitlab.com/api/v4/`.
47
+ """
48
+ return f"https://{self.host}/api/v4/"
49
+
50
+ def _get_headers(self) -> dict:
51
+ """
52
+ Creates headers for a GitLab REST API call.
53
+
54
+ The headers contain token for authentication according to the
55
+ instance variables.
56
+
57
+ Returns
58
+ -------
59
+ dict
60
+ Headers for a REST API request, that contain the authentication token.
61
+ """
62
+ headers = {}
63
+ if self.token_type and self.token:
64
+ headers = {self.token_type: self.token}
65
+ return headers
66
+
67
+ def _parse_header_links(self, headers: HTTPMessage) -> dict:
68
+ """
69
+ Parses link field from HTTP headers to a dictionary where
70
+ the link "rel" value is the key.
71
+
72
+ This is useful for example with GitLab REST API to get the pagination links.
73
+
74
+ Parameters
75
+ ----------
76
+ headers : HTTPMessage
77
+ The HTTP response headers that contain the links
78
+
79
+ Returns
80
+ -------
81
+ dict
82
+ The header links in a dictionary, that can be used to for example pagination:
83
+ _parse_header_links(headers).get("next") returns the next page link, or None.
84
+ """
85
+ links = {}
86
+ header_links = headers.get("link")
87
+ if header_links:
88
+ items = header_links.split(",")
89
+ # Values are <uri-reference>; param1=value1; param2="value2"
90
+ for item in items:
91
+ parts = item.split(";", 1)
92
+ if parts:
93
+ # First value should be the URI
94
+ val = parts.pop(0).lstrip(" <").rstrip("> ")
95
+ # The rest are the parameters; let's take the first one that has rel=
96
+ # in it, split it with ; and take the first part as a value
97
+ typ = parts.pop()
98
+ typ = typ.split("rel=", 1).pop().split(";").pop(0).strip('" ')
99
+ links[typ] = val
100
+ return links
101
+
102
+ def _build_query(self, **args: str) -> str:
103
+ """
104
+ Builds a query for a GitLab REST API request
105
+
106
+ Parameters
107
+ ----------
108
+ args : str
109
+ keyword arguments for the REST API query, like per_page=20, page=20
110
+
111
+ Returns
112
+ -------
113
+ str
114
+ A query string for a REST API request. Append this to
115
+ the request URL. Example `?per_page=20&page=20`.
116
+ """
117
+ query = ""
118
+ for key, value in args.items():
119
+ query = "&".join(filter(None, (query, f"{key}={parse.quote_plus(value)}")))
120
+ if query:
121
+ query = "?" + query
122
+ return query
123
+
124
+ def _build_path(self, *paths: str) -> str:
125
+ """
126
+ Returns the path to be appended to the REST API URL
127
+
128
+ Parameters
129
+ ----------
130
+ paths : str
131
+ The path string that are joined with "/".
132
+
133
+ Returns
134
+ -------
135
+ str
136
+ A URL path, for example `projects/123/`
137
+ or `projects/namespace%2Fproject/`
138
+ """
139
+ quoted_paths = []
140
+ for subpath in paths:
141
+ quoted_paths.append(subpath)
142
+ return "/".join(quoted_paths)
143
+
144
+ def get(self, *paths: str, **query_params: str) -> tuple[int, bytes, HTTPMessage]:
145
+ """
146
+ Makes a HTTP GET request to the given GitLab path, and returns
147
+ the response status, body, and headers.
148
+
149
+ Parameters
150
+ ----------
151
+ paths : str
152
+ The URL path of the HTTP request to make.
153
+ query_params : str, optional
154
+ Dictionary of query parameters for the request, like package_name=mypackage
155
+
156
+ Returns
157
+ -------
158
+ int
159
+ The HTTP response code, such as 200
160
+ bytes
161
+ The HTTP response body read as bytes
162
+ HTTPMessage
163
+ The HTTP response headers
164
+ """
165
+ url = self._url() + self._build_path(*paths) + self._build_query(**query_params)
166
+ headers = self._get_headers()
167
+ return http.get(url, headers)
168
+
169
+ def get_all(self, *paths: str, **query_params: str) -> list:
170
+ """
171
+ Returns data from the REST API endpoint. In case
172
+ of multiple pages, all data will be returned.
173
+
174
+ Parameters
175
+ ----------
176
+ paths : str
177
+ The paths of the API endpoint that is called. For example projects, 123, packages
178
+ would be querying the projects/123/packages endpoint.
179
+ query_params : str, optional
180
+ Additional arguments for the query of the URL, for example to filter
181
+ results: package_name=mypackage
182
+
183
+ Returns
184
+ -------
185
+ list
186
+ Data from GitLab REST API endpoint with the arguments.
187
+ """
188
+ url = self._url() + self._build_path(*paths) + self._build_query(**query_params)
189
+ data = []
190
+ while url:
191
+ res_status, res_data, res_headers = http.get(url, self._get_headers())
192
+ logger.debug("Response status: %d", res_status)
193
+ res_data = loads(res_data)
194
+ logger.debug("Response data: %s", res_data)
195
+ data = data + res_data
196
+ url = self._parse_header_links(res_headers).get("next")
197
+ return data
198
+
199
+ def put(self, data: bytes, *paths: str, **query_params: str) -> int:
200
+ """
201
+ Makes a HTTP PUT request to the given GitLab path.
202
+
203
+ Parameters
204
+ ----------
205
+ data : bytes
206
+ The data to PUT.
207
+ paths : str
208
+ The URL path of the HTTP request to make.
209
+ query_params : str, optional
210
+ Additional arguments for the query of the URL, for example to filter
211
+ results: package_name=mypackage
212
+
213
+ Returns
214
+ -------
215
+ int
216
+ 0 If the request was successful.
217
+ """
218
+ ret = 1
219
+ url = self._url() + self._build_path(*paths) + self._build_query(**query_params)
220
+ status, _, _ = http.put(url, data, self._get_headers())
221
+ if status == 201: # 201 is created
222
+ ret = 0
223
+ return ret
224
+
225
+ def delete(self, *paths: str, **query_params: str) -> int:
226
+ """
227
+ Makes a HTTP DELETE request to the given GitLab path.
228
+
229
+ Parameters
230
+ ----------
231
+ paths : str
232
+ The URL path of the HTTP request to make.
233
+ query_params : str, optional
234
+ Additional arguments for the query of the URL, for example to filter
235
+ results: package_name=mypackage
236
+
237
+ Returns
238
+ -------
239
+ int
240
+ 0 If the request was successful.
241
+ """
242
+ ret = 1
243
+ url = self._url() + self._build_path(*paths) + self._build_query(**query_params)
244
+ status, _, _ = http.delete(url, self._get_headers())
245
+ if status == 204:
246
+ # 204 is no content, that GL responds when file deleted
247
+ ret = 0
248
+ return ret
gitlab/lib/http.py ADDED
@@ -0,0 +1,90 @@
1
+ """HTTP helper"""
2
+
3
+ from http.client import HTTPMessage
4
+ import logging
5
+ from urllib import request
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ def get(url: str, headers: dict) -> tuple[int, bytes, HTTPMessage]:
11
+ """
12
+ Makes a raw GET request to the given URL, and returns
13
+ the response status, body, and headers.
14
+
15
+ Parameters
16
+ ----------
17
+ url : str
18
+ The URL of the HTTP request to make.
19
+ headers: dict
20
+ The HTTP headers used in the request.
21
+
22
+ Returns
23
+ -------
24
+ int
25
+ The HTTP response code, such as 200
26
+ bytes
27
+ The HTTP response body read as bytes
28
+ HTTPMessage
29
+ The HTTP response headers
30
+ """
31
+ logger.debug("Getting %s", url)
32
+ req = request.Request(url, headers=headers)
33
+ with request.urlopen(req) as response:
34
+ return response.status, response.read(), response.headers
35
+
36
+
37
+ def put(url: str, data: bytes, headers: dict) -> tuple[int, bytes, HTTPMessage]:
38
+ """
39
+ Makes a raw PUT request to the given URL, and returns
40
+ the response status, body, and headers.
41
+
42
+ Parameters
43
+ ----------
44
+ url : str
45
+ The URL of the HTTP request to make.
46
+ data : bytes
47
+ The data to PUT
48
+ headers: dict
49
+ The HTTP headers used in the request.
50
+
51
+ Returns
52
+ -------
53
+ int
54
+ The HTTP response code, such as 200
55
+ bytes
56
+ The HTTP response body read as bytes
57
+ HTTPMessage
58
+ The HTTP response headers
59
+ """
60
+ logger.debug("Putting %s", url)
61
+ req = request.Request(url, method="PUT", data=data, headers=headers)
62
+ with request.urlopen(req) as response:
63
+ return response.status, response.read(), response.headers
64
+
65
+
66
+ def delete(url, headers):
67
+ """
68
+ Makes a raw DELETE request to the given URL, and returns
69
+ the response status, body, and headers.
70
+
71
+ Parameters
72
+ ----------
73
+ url : str
74
+ The URL of the HTTP request to make.
75
+ headers: dict
76
+ The HTTP headers used in the request.
77
+
78
+ Returns
79
+ -------
80
+ int
81
+ The HTTP response code, such as 200
82
+ bytes
83
+ The HTTP response body read as bytes
84
+ HTTPMessage
85
+ The HTTP response headers
86
+ """
87
+ logger.debug("Deleting %s", url)
88
+ req = request.Request(url, method="DELETE", headers=headers)
89
+ with request.urlopen(req) as response:
90
+ return response.status, response.read(), response.headers
gitlab/packages.py CHANGED
@@ -1,11 +1,10 @@
1
1
  """GitLab generic packages module"""
2
2
 
3
3
  from glob import glob
4
- from http.client import HTTPMessage
5
- from json import loads
6
4
  import logging
7
5
  import os
8
- from urllib import request, parse
6
+ from urllib import parse
7
+ from gitlab.lib.gitlab_api import GitLabAPI
9
8
 
10
9
  logger = logging.getLogger(__name__)
11
10
 
@@ -33,302 +32,7 @@ class Packages:
33
32
  This can be a personal token, project token, or`CI_JOB_TOKEN`.
34
33
  Leave empty when authentication is not used.
35
34
  """
36
- self.host = host
37
- self.token_type = token_type
38
- self.token = token
39
-
40
- def _url(self) -> str:
41
- """
42
- Returns the GitLab REST API URL by using the host variable.
43
-
44
- Returns
45
- -------
46
- str
47
- The GitLab REST API URL, for example `https://gitlab.com/api/v4/`.
48
- """
49
- return f"https://{self.host}/api/v4/"
50
-
51
- def _get_headers(self) -> dict:
52
- """
53
- Creates headers for a GitLab REST API call.
54
-
55
- The headers contain token for authentication according to the
56
- instance variables.
57
-
58
- Returns
59
- -------
60
- dict
61
- Headers for a REST API request, that contain the authentication token.
62
- """
63
- headers = {}
64
- if self.token_type and self.token:
65
- headers = {self.token_type: self.token}
66
- return headers
67
-
68
- def _parse_header_links(self, headers: HTTPMessage) -> dict:
69
- """
70
- Parses link field from HTTP headers to a dictionary where
71
- the link "rel" value is the key.
72
-
73
- This is useful for example with GitLab REST API to get the pagination links.
74
-
75
- Parameters
76
- ----------
77
- headers : HTTPMessage
78
- The HTTP response headers that contain the links
79
-
80
- Returns
81
- -------
82
- dict
83
- The header links in a dictionary, that can be used to for example pagination:
84
- _parse_header_links(headers).get("next") returns the next page link, or None.
85
- """
86
- links = {}
87
- header_links = headers.get("link")
88
- if header_links:
89
- items = header_links.split(",")
90
- # Values are <uri-reference>; param1=value1; param2="value2"
91
- for item in items:
92
- parts = item.split(";", 1)
93
- if parts:
94
- # First value should be the URI
95
- val = parts.pop(0).lstrip(" <").rstrip("> ")
96
- # The rest are the parameters; let's take the first one that has rel=
97
- # in it, split it with ; and take the first part as a value
98
- typ = parts.pop()
99
- typ = typ.split("rel=", 1).pop().split(";").pop(0).strip('" ')
100
- links[typ] = val
101
- return links
102
-
103
- def _build_path(self, *paths: str) -> str:
104
- """
105
- Returns the path to be appended to the REST API URL
106
-
107
- Parameters
108
- ----------
109
- paths : str
110
- The path string that are joined with "/".
111
-
112
- Returns
113
- -------
114
- str
115
- A URL path, for example `projects/123/`
116
- or `projects/namespace%2Fproject/`
117
- """
118
- quoted_paths = []
119
- for subpath in paths:
120
- quoted_paths.append(subpath)
121
- return "/".join(quoted_paths)
122
-
123
- def build_query(self, **args: str) -> str:
124
- """
125
- Builds a query for a GitLab REST API request
126
-
127
- Parameters
128
- ----------
129
- args : str
130
- keyword arguments for the REST API query, like per_page=20, page=20
131
-
132
- Returns
133
- -------
134
- str
135
- A query string for a REST API request. Append this to
136
- the request URL. Example `?per_page=20&page=20`.
137
- """
138
- query = ""
139
- for key, value in args.items():
140
- query = "&".join(filter(None, (query, f"{key}={parse.quote_plus(value)}")))
141
- if query:
142
- query = "?" + query
143
- return query
144
-
145
- def _get(self, url: str, headers: dict) -> tuple[int, bytes, HTTPMessage]:
146
- """
147
- Makes a raw GET request to the given URL, and returns
148
- the response status, body, and headers.
149
-
150
- Parameters
151
- ----------
152
- url : str
153
- The URL of the HTTP request to make.
154
- headers: dict
155
- The HTTP headers used in the request.
156
-
157
- Returns
158
- -------
159
- int
160
- The HTTP response code, such as 200
161
- bytes
162
- The HTTP response body read as bytes
163
- HTTPMessage
164
- The HTTP response headers
165
- """
166
- logger.debug("Getting %s", url)
167
- req = request.Request(url, headers=headers)
168
- with request.urlopen(req) as response:
169
- return response.status, response.read(), response.headers
170
-
171
- def get(self, *paths: str, **query_params: str) -> tuple[int, bytes, HTTPMessage]:
172
- """
173
- Makes a HTTP GET request to the given GitLab path, and returns
174
- the response status, body, and headers.
175
-
176
- Parameters
177
- ----------
178
- paths : str
179
- The URL path of the HTTP request to make.
180
- query_params : str, optional
181
- Dictionary of query parameters for the request, like package_name=mypackage
182
-
183
- Returns
184
- -------
185
- int
186
- The HTTP response code, such as 200
187
- bytes
188
- The HTTP response body read as bytes
189
- HTTPMessage
190
- The HTTP response headers
191
- """
192
- url = self._url() + self._build_path(*paths) + self.build_query(**query_params)
193
- headers = self._get_headers()
194
- return self._get(url, headers)
195
-
196
- def get_all(self, *paths: str, **query_params: str) -> list:
197
- """
198
- Returns data from the REST API endpoint. In case
199
- of multiple pages, all data will be returned.
200
-
201
- Parameters
202
- ----------
203
- paths : str
204
- The paths of the API endpoint that is called. For example projects, 123, packages
205
- would be querying the projects/123/packages endpoint.
206
- query_params : str, optional
207
- Additional arguments for the query of the URL, for example to filter
208
- results: package_name=mypackage
209
-
210
- Returns
211
- -------
212
- list
213
- Data from GitLab REST API endpoint with the arguments.
214
- """
215
- url = self._url() + self._build_path(*paths) + self.build_query(**query_params)
216
- data = []
217
- while url:
218
- res_status, res_data, res_headers = self._get(url, self._get_headers())
219
- logger.debug("Response status: %d", res_status)
220
- res_data = loads(res_data)
221
- logger.debug("Response data: %s", res_data)
222
- data = data + res_data
223
- url = self._parse_header_links(res_headers).get("next")
224
- return data
225
-
226
- def _put(
227
- self, url: str, data: bytes, headers: dict
228
- ) -> tuple[int, bytes, HTTPMessage]:
229
- """
230
- Makes a raw PUT request to the given URL, and returns
231
- the response status, body, and headers.
232
-
233
- Parameters
234
- ----------
235
- url : str
236
- The URL of the HTTP request to make.
237
- data : bytes
238
- The data to PUT
239
- headers: dict
240
- The HTTP headers used in the request.
241
-
242
- Returns
243
- -------
244
- int
245
- The HTTP response code, such as 200
246
- bytes
247
- The HTTP response body read as bytes
248
- HTTPMessage
249
- The HTTP response headers
250
- """
251
- logger.debug("Putting %s", url)
252
- req = request.Request(url, method="PUT", data=data, headers=headers)
253
- with request.urlopen(req) as response:
254
- return response.status, response.read(), response.headers
255
-
256
- def put(self, data: bytes, *paths: str, **query_params: str) -> int:
257
- """
258
- Makes a HTTP PUT request to the given GitLab path.
259
-
260
- Parameters
261
- ----------
262
- data : bytes
263
- The data to PUT.
264
- paths : str
265
- The URL path of the HTTP request to make.
266
- query_params : str, optional
267
- Additional arguments for the query of the URL, for example to filter
268
- results: package_name=mypackage
269
-
270
- Returns
271
- -------
272
- int
273
- 0 If the request was successful.
274
- """
275
- ret = 1
276
- url = self._url() + self._build_path(*paths) + self.build_query(**query_params)
277
- status, _, _ = self._put(url, data, self._get_headers())
278
- if status == 201: # 201 is created
279
- ret = 0
280
- return ret
281
-
282
- def _delete(self, url, headers):
283
- """
284
- Makes a raw DELETE request to the given URL, and returns
285
- the response status, body, and headers.
286
-
287
- Parameters
288
- ----------
289
- url : str
290
- The URL of the HTTP request to make.
291
- headers: dict
292
- The HTTP headers used in the request.
293
-
294
- Returns
295
- -------
296
- int
297
- The HTTP response code, such as 200
298
- bytes
299
- The HTTP response body read as bytes
300
- HTTPMessage
301
- The HTTP response headers
302
- """
303
- logger.debug("Deleting %s", url)
304
- req = request.Request(url, method="DELETE", headers=headers)
305
- with request.urlopen(req) as response:
306
- return response.status, response.read(), response.headers
307
-
308
- def delete(self, *paths: str, **query_params: str) -> int:
309
- """
310
- Makes a HTTP DELETE request to the given GitLab path.
311
-
312
- Parameters
313
- ----------
314
- paths : str
315
- The URL path of the HTTP request to make.
316
- query_params : str, optional
317
- Additional arguments for the query of the URL, for example to filter
318
- results: package_name=mypackage
319
-
320
- Returns
321
- -------
322
- int
323
- 0 If the request was successful.
324
- """
325
- ret = 1
326
- url = self._url() + self._build_path(*paths) + self.build_query(**query_params)
327
- status, _, _ = self._delete(url, self._get_headers())
328
- if status == 204:
329
- # 204 is no content, that GL responds when file deleted
330
- ret = 0
331
- return ret
35
+ self.gl_api = GitLabAPI(host, token_type, token)
332
36
 
333
37
  def get_versions(self, project_id: str, package_name: str) -> list:
334
38
  """
@@ -349,7 +53,7 @@ class Packages:
349
53
  """
350
54
  packages = []
351
55
  logger.debug("Listing packages with name %s", package_name)
352
- data = self.get_all(
56
+ data = self.gl_api.get_all(
353
57
  "projects",
354
58
  parse.quote_plus(project_id),
355
59
  "packages",
@@ -387,7 +91,7 @@ class Packages:
387
91
  """
388
92
  files = {}
389
93
  logger.debug("Listing package %d files", package_id)
390
- data = self.get_all(
94
+ data = self.gl_api.get_all(
391
95
  "projects",
392
96
  parse.quote_plus(project_id),
393
97
  "packages",
@@ -425,7 +129,7 @@ class Packages:
425
129
  """
426
130
  package_id = -1
427
131
  logger.debug("Fetching package %s (%s) ID", package_name, package_version)
428
- data = self.get_all(
132
+ data = self.gl_api.get_all(
429
133
  "projects",
430
134
  parse.quote_plus(project_id),
431
135
  "packages",
@@ -438,6 +142,48 @@ class Packages:
438
142
  package_id = package["id"]
439
143
  return package_id
440
144
 
145
+ def download(
146
+ self,
147
+ project_id: str,
148
+ package_name: str,
149
+ package_version: str,
150
+ destination: str = "",
151
+ ) -> int:
152
+ """
153
+ Downloads a package from a GitLab generic package registry
154
+
155
+ Parameters
156
+ ----------
157
+ project_id : str
158
+ The project ID or path, including namespace.
159
+ Examples: `123` or `namespace/project`.
160
+ package_name : str
161
+ The name of the generic package.
162
+ package_version : str
163
+ The version of the generic package
164
+ destination : str, optional
165
+ The destination folder of the downloaded file. If not set,
166
+ current working directory is used.
167
+
168
+ Return
169
+ ------
170
+ int
171
+ Zero if everything went fine, non-zero coke otherwise.
172
+ Two in case package was not found.
173
+ """
174
+ ret = 2
175
+ package_id = self.get_id(project_id, package_name, package_version)
176
+ if package_id:
177
+ files = []
178
+ files = self.get_files(project_id, package_id).keys()
179
+ for file in files:
180
+ ret = self.download_file(
181
+ project_id, package_name, package_version, file, destination
182
+ )
183
+ if ret:
184
+ break
185
+ return ret
186
+
441
187
  def download_file(
442
188
  self,
443
189
  project_id: str,
@@ -471,7 +217,7 @@ class Packages:
471
217
  """
472
218
  ret = 1
473
219
  logger.debug("Downloading file %s", filename)
474
- status, data, _ = self.get(
220
+ status, data, _ = self.gl_api.get(
475
221
  "projects",
476
222
  parse.quote_plus(project_id),
477
223
  "packages",
@@ -493,12 +239,11 @@ class Packages:
493
239
  ret = 0
494
240
  return ret
495
241
 
496
- def upload_file(
242
+ def upload(
497
243
  self,
498
244
  project_id: str,
499
245
  package_name: str,
500
246
  package_version: str,
501
- filename: str,
502
247
  source: str,
503
248
  ) -> int:
504
249
  """
@@ -513,9 +258,6 @@ class Packages:
513
258
  The name of the generic package.
514
259
  package_version : str
515
260
  The version of the generic package
516
- pfile : str
517
- The relative path of the file that is uploaded. If left empty,
518
- all files from the source folder, and it's subfolders, are uploaded.
519
261
  source : str
520
262
  The source folder that is used as root when uploading. If empty,
521
263
  current working directory is used.
@@ -527,24 +269,21 @@ class Packages:
527
269
  """
528
270
  files = []
529
271
  ret = 1
530
- if filename:
531
- files.append(filename)
532
- else:
533
- filelist = glob(os.path.join(source, "**"), recursive=True)
534
- for item in filelist:
535
- # Only add files, not folders
536
- if os.path.isfile(os.path.join(item)):
537
- # Remove the source folder from the path of the files
538
- files.append(os.path.relpath(item, source))
272
+ filelist = glob(os.path.join(source, "**"), recursive=True)
273
+ for item in filelist:
274
+ # Only add files, not folders
275
+ if os.path.isfile(os.path.join(item)):
276
+ # Remove the source folder from the path of the files
277
+ files.append(os.path.relpath(item, source))
539
278
  for afile in files:
540
- ret = self._upload_file(
279
+ ret = self.upload_file(
541
280
  project_id, package_name, package_version, afile, source
542
281
  )
543
282
  if ret:
544
283
  break
545
284
  return ret
546
285
 
547
- def _upload_file(
286
+ def upload_file(
548
287
  self,
549
288
  project_id: str,
550
289
  package_name: str,
@@ -578,7 +317,7 @@ class Packages:
578
317
  fpath = os.path.join(source, filename)
579
318
  logger.debug("Uploading file %s from %s", filename, source)
580
319
  with open(fpath, "rb") as data:
581
- ret = self.put(
320
+ ret = self.gl_api.put(
582
321
  data.read(),
583
322
  "projects",
584
323
  parse.quote_plus(project_id),
@@ -590,9 +329,7 @@ class Packages:
590
329
  )
591
330
  return ret
592
331
 
593
- def delete_package(
594
- self, project_id: str, package_name: str, package_version: str
595
- ) -> int:
332
+ def delete(self, project_id: str, package_name: str, package_version: str) -> int:
596
333
  """
597
334
  Deletes a version of a GitLab generic package.
598
335
 
@@ -614,7 +351,7 @@ class Packages:
614
351
  ret = 1
615
352
  package_id = self.get_id(project_id, package_name, package_version)
616
353
  if package_id > 0:
617
- ret = self.delete(
354
+ ret = self.gl_api.delete(
618
355
  "projects", parse.quote_plus(project_id), "packages", str(package_id)
619
356
  )
620
357
  return ret
@@ -664,7 +401,7 @@ class Packages:
664
401
  package_id,
665
402
  project_id,
666
403
  )
667
- ret = self.delete(
404
+ ret = self.gl_api.delete(
668
405
  "projects",
669
406
  parse.quote_plus(project_id),
670
407
  "packages",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: glpkg
3
- Version: 1.4.2
3
+ Version: 1.5.0
4
4
  Summary: Tool to make GitLab generic package registry operations easy.
5
5
  Author-email: bugproduction <bugproduction@outlook.com>
6
6
  License-Expression: MIT
@@ -101,6 +101,29 @@ glpkg download --project 12345 --name mypackagename --version 1.5 --file the_onl
101
101
 
102
102
  > If a package has multiple files with the same filename, the tool can only download the newest file. This is a restriction of GitLab API.
103
103
 
104
+ #### Downloading a list of packages
105
+
106
+ To download multiple packages or versions from one or more projects, a package file can be used together with `--from-file` argument with `download`:
107
+
108
+ ```bash
109
+ glpkg download --from-file my-packages.txt --destination my-downloads
110
+ ```
111
+
112
+ The `my-packages.txt` lists all wanted packages with their versions and projects in format `<package-name>==<package-version>@<project-id>`.For example `my-packages.txt` could look like this:
113
+
114
+ ```
115
+ mypackage==2.3.6-beta@namespace/project
116
+ yourpackage==1.2.3@12345
117
+ ```
118
+
119
+ The `<project-id>` can be either the path to the project in string format or the project ID in numeric.
120
+
121
+ When `--from-file` argument is used, `--file`, `--project`, `--version`, and `--name` arguments are unused.
122
+
123
+ > The `--destination` argument can be used to download the files from the packages to a different folder. In case packages contains files with same names, some files will be overwritten.
124
+
125
+ > Whatever credentials (`--token`, `--ci`, or `--netrc`) you run the download command with will be used for all package downloads in the list. Make sure that the credential has access to all projects.
126
+
104
127
  ### Upload a file to a generic package
105
128
 
106
129
  To upload a file to a version of a generic package, run
@@ -158,7 +181,7 @@ The token that is used to delete packages must have at least Maintainer role in
158
181
 
159
182
  ### Use in GitLab pipelines
160
183
 
161
- If you use the tool in a GitLab pipeline, setting argument `--ci` uses [GitLab predefined variables](https://docs.gitlab.com/ci/variables/predefined_variables/) to configure the tool. In this case `CI_SERVER_HOST`, `CI_PROJECT_ID`, and `CI_JOB_TOKEN` environment variables are used. The `--project`, and `--token` arguments can still be used to override the project ID or to use a personal or project access token instead of `CI_JOB_TOKEN`.
184
+ If you use the tool in a GitLab pipeline, setting argument `--ci` uses [GitLab predefined variables](https://docs.gitlab.com/ci/variables/predefined_variables/) to configure the tool. In this case `CI_SERVER_HOST`, `CI_PROJECT_ID`, and `CI_JOB_TOKEN` environment variables are used. The `--host`, `--project`, and `--token` arguments can still be used to override the host, project ID, or to use a personal or project access token instead of `CI_JOB_TOKEN`.
162
185
 
163
186
  In other words, you don't need to give the `--host`, `--project`, or `--token` arguments if you are interacting with the package registry of the project where the pipeline is running. Example: uploading `my-file.txt` to generic package `mypackagename` version `1.0` in the project package registry in CI:
164
187
 
@@ -0,0 +1,12 @@
1
+ gitlab/__init__.py,sha256=_Gr_P6JuBFaW50vk2fuhb4-PspiCUUCDot6pPQZhrZM,105
2
+ gitlab/__main__.py,sha256=ol-xxwX5PONbnqEsq5w-5_bowDyg9Zu-eJZG5XrJeFc,427
3
+ gitlab/cli_handler.py,sha256=LorqYsco1bL8up_2QyAlku0L6p_8Cp_twvaKpCNmEuQ,15921
4
+ gitlab/packages.py,sha256=6m2SkqA-wNyysgqCIToBUM1uy9VH0iegQZCIaoYxq6s,13179
5
+ gitlab/lib/gitlab_api.py,sha256=a__xlefBIX-sEIV21wVHVaZxwRrulI-_RoNKwXnX8JM,8222
6
+ gitlab/lib/http.py,sha256=1QgH8unBNCg6709EhYH0MhtjlcAjzWQ7TFqB5T0-pgY,2344
7
+ glpkg-1.5.0.dist-info/licenses/LICENSE.md,sha256=josGXvZq628dNS0Iru58-DPE7dRpDXzjJxKKT35103g,1065
8
+ glpkg-1.5.0.dist-info/METADATA,sha256=2rhVd60bHadPleyxCU4UUu6jlnJX8J59m60tWkFUqmU,8889
9
+ glpkg-1.5.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
10
+ glpkg-1.5.0.dist-info/entry_points.txt,sha256=xHPZwx2oShYDZ3AyH7WSIvuhFMssy7QLlQk-JAbje_w,46
11
+ glpkg-1.5.0.dist-info/top_level.txt,sha256=MvIaP8p_Oaf4gO_hXmHkX-5y2deHLp1pe6tJR3ukQ6o,7
12
+ glpkg-1.5.0.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- gitlab/__init__.py,sha256=zDp9HG0fq7XbOi99n4lMzur9Yeb1uhtfTJoZQxwAW3w,105
2
- gitlab/__main__.py,sha256=ol-xxwX5PONbnqEsq5w-5_bowDyg9Zu-eJZG5XrJeFc,427
3
- gitlab/cli_handler.py,sha256=_iQCOUli5jXrXD8trnh8KFCSQQXXOEMZrkTRfmE797Q,13854
4
- gitlab/packages.py,sha256=lIIo14EVm8lOwgZKug_w7txWppiqx6N0D0zLqpbbJ8c,21729
5
- glpkg-1.4.2.dist-info/licenses/LICENSE.md,sha256=josGXvZq628dNS0Iru58-DPE7dRpDXzjJxKKT35103g,1065
6
- glpkg-1.4.2.dist-info/METADATA,sha256=Sh4rBcnF7W47p-K8mw6rA5OTQmONmYAT0LTQUkbD_Gw,7730
7
- glpkg-1.4.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
8
- glpkg-1.4.2.dist-info/entry_points.txt,sha256=xHPZwx2oShYDZ3AyH7WSIvuhFMssy7QLlQk-JAbje_w,46
9
- glpkg-1.4.2.dist-info/top_level.txt,sha256=MvIaP8p_Oaf4gO_hXmHkX-5y2deHLp1pe6tJR3ukQ6o,7
10
- glpkg-1.4.2.dist-info/RECORD,,
File without changes