glpkg 1.4.1__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.1"
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__
@@ -21,9 +22,8 @@ class CLIHandler:
21
22
  parser = argparse.ArgumentParser(
22
23
  description="Toolbox for GitLab generic packages"
23
24
  )
24
- parser.add_argument("-v", "--version", action="store_true")
25
- parser.set_defaults(action=self._print_version)
26
- subparsers = parser.add_subparsers()
25
+ parser.add_argument("-v", "--version", action="version", version=__version__)
26
+ subparsers = parser.add_subparsers(required=True)
27
27
  list_parser = subparsers.add_parser(
28
28
  name="list",
29
29
  description="Lists the available version of a package from the "
@@ -49,23 +49,6 @@ class CLIHandler:
49
49
  self._register_delete_parser(delete_parser)
50
50
  self.args = parser.parse_args()
51
51
 
52
- def _print_version(self, _args: argparse.Namespace) -> int:
53
- """
54
- A handler for printing the version of the tool to the console.
55
-
56
- Parameters
57
- ----------
58
- _args : argparse.Namespace
59
- Unused.
60
-
61
- Return
62
- ------
63
- int
64
- Zero when printing to console succeeded.
65
- """
66
- print(__version__)
67
- return 0
68
-
69
52
  def do_it(self) -> int:
70
53
  """
71
54
  Executes the requested command.
@@ -90,6 +73,9 @@ class CLIHandler:
90
73
  file=sys.stderr,
91
74
  )
92
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)
93
79
  return ret
94
80
 
95
81
  def _register_common_arguments(self, parser: argparse.ArgumentParser) -> None:
@@ -107,7 +93,7 @@ class CLIHandler:
107
93
  parser:
108
94
  The argparser where to register the common arguments.
109
95
  """
110
- group = parser.add_mutually_exclusive_group()
96
+ group = parser.add_argument_group()
111
97
  group.add_argument(
112
98
  "-H",
113
99
  "--host",
@@ -166,6 +152,12 @@ class CLIHandler:
166
152
  """
167
153
  self._register_common_arguments(parser)
168
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
+ )
169
161
  parser.add_argument(
170
162
  "-f",
171
163
  "--file",
@@ -213,6 +205,8 @@ class CLIHandler:
213
205
  project = os.environ["CI_PROJECT_ID"]
214
206
  token = os.environ["CI_JOB_TOKEN"]
215
207
  token_user = "JOB-TOKEN"
208
+ if args.host:
209
+ host = args.host
216
210
  if args.project:
217
211
  project = args.project
218
212
  if args.token:
@@ -247,23 +241,67 @@ class CLIHandler:
247
241
  host, project, name, token_user, token = self._args(args)
248
242
  version = args.version
249
243
  destination = args.destination
244
+ file = args.file
245
+ from_file = args.from_file
250
246
  packages = Packages(host, token_user, token)
251
- package_id = packages.get_id(project, name, version)
252
- if package_id:
253
- files = []
254
- if args.file:
255
- files.append(args.file)
256
- else:
257
- files = packages.get_files(project, package_id).keys()
258
- for file in files:
259
- 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
+ )
260
253
  if ret:
261
- print("Failed to download file " + file)
262
254
  break
255
+ elif file:
256
+ ret = packages.download_file(project, name, version, file, destination)
263
257
  else:
264
- 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)!")
265
264
  return ret
266
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
+
267
305
  def _register_list_parser(self, parser: argparse.ArgumentParser):
268
306
  """
269
307
  Registers the list command related arguments to the parser:
@@ -352,19 +390,21 @@ class CLIHandler:
352
390
  int
353
391
  Zero if everything went fine, non-zero otherwise.
354
392
  """
355
- ret = 0
393
+ ret = 1
356
394
  host, project, name, token_user, token = self._args(args)
357
395
  version = args.version
358
396
  file = args.file
359
397
  source = args.source
398
+ packages = Packages(host, token_user, token)
360
399
  if file:
361
400
  # Check if the uploaded file exists.
362
401
  if not os.path.isfile(os.path.join(source, file)):
363
402
  print("File " + file + " does not exist!")
364
403
  ret = 1
365
- if not ret:
366
- packages = Packages(host, token_user, token)
367
- 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)
368
408
  return ret
369
409
 
370
410
  def _register_delete_parser(self, parser: argparse.ArgumentParser):
@@ -415,5 +455,5 @@ class CLIHandler:
415
455
  if file:
416
456
  ret = packages.delete_file(project, name, version, file)
417
457
  else:
418
- ret = packages.delete_package(project, name, version)
458
+ ret = packages.delete(project, name, version)
419
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,250 +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(self, data: bytes, *paths: str, **query_params: str) -> int:
227
- """
228
- Makes a HTTP PUT request to the given GitLab path.
229
-
230
- Parameters
231
- ----------
232
- data : bytes
233
- The data to PUT.
234
- paths : str
235
- The URL path of the HTTP request to make.
236
- query_params : str, optional
237
- Additional arguments for the query of the URL, for example to filter
238
- results: package_name=mypackage
239
-
240
- Returns
241
- -------
242
- int
243
- 0 If the request was successful.
244
- """
245
- ret = 1
246
- url = self._url() + self._build_path(*paths) + self.build_query(**query_params)
247
- logger.debug("Putting %s", url)
248
- req = request.Request(url, method="PUT", data=data, headers=self._get_headers())
249
- with request.urlopen(req) as res:
250
- if res.status == 201: # 201 is created
251
- ret = 0
252
- return ret
253
-
254
- def delete(self, *paths: str, **query_params: str) -> int:
255
- """
256
- Makes a HTTP DELETE request to the given GitLab path.
257
-
258
- Parameters
259
- ----------
260
- paths : str
261
- The URL path of the HTTP request to make.
262
- query_params : str, optional
263
- Additional arguments for the query of the URL, for example to filter
264
- results: package_name=mypackage
265
-
266
- Returns
267
- -------
268
- int
269
- 0 If the request was successful.
270
- """
271
- ret = 1
272
- url = self._url() + self._build_path(*paths) + self.build_query(**query_params)
273
- logger.debug("Deleting %s", url)
274
- req = request.Request(url, method="DELETE", headers=self._get_headers())
275
- with request.urlopen(req) as res:
276
- if res.status == 204:
277
- # 204 is no content, that GL responds when file deleted
278
- ret = 0
279
- return ret
35
+ self.gl_api = GitLabAPI(host, token_type, token)
280
36
 
281
37
  def get_versions(self, project_id: str, package_name: str) -> list:
282
38
  """
@@ -297,7 +53,7 @@ class Packages:
297
53
  """
298
54
  packages = []
299
55
  logger.debug("Listing packages with name %s", package_name)
300
- data = self.get_all(
56
+ data = self.gl_api.get_all(
301
57
  "projects",
302
58
  parse.quote_plus(project_id),
303
59
  "packages",
@@ -335,7 +91,7 @@ class Packages:
335
91
  """
336
92
  files = {}
337
93
  logger.debug("Listing package %d files", package_id)
338
- data = self.get_all(
94
+ data = self.gl_api.get_all(
339
95
  "projects",
340
96
  parse.quote_plus(project_id),
341
97
  "packages",
@@ -373,7 +129,7 @@ class Packages:
373
129
  """
374
130
  package_id = -1
375
131
  logger.debug("Fetching package %s (%s) ID", package_name, package_version)
376
- data = self.get_all(
132
+ data = self.gl_api.get_all(
377
133
  "projects",
378
134
  parse.quote_plus(project_id),
379
135
  "packages",
@@ -386,6 +142,48 @@ class Packages:
386
142
  package_id = package["id"]
387
143
  return package_id
388
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
+
389
187
  def download_file(
390
188
  self,
391
189
  project_id: str,
@@ -419,7 +217,7 @@ class Packages:
419
217
  """
420
218
  ret = 1
421
219
  logger.debug("Downloading file %s", filename)
422
- status, data, _ = self.get(
220
+ status, data, _ = self.gl_api.get(
423
221
  "projects",
424
222
  parse.quote_plus(project_id),
425
223
  "packages",
@@ -441,12 +239,11 @@ class Packages:
441
239
  ret = 0
442
240
  return ret
443
241
 
444
- def upload_file(
242
+ def upload(
445
243
  self,
446
244
  project_id: str,
447
245
  package_name: str,
448
246
  package_version: str,
449
- filename: str,
450
247
  source: str,
451
248
  ) -> int:
452
249
  """
@@ -461,9 +258,6 @@ class Packages:
461
258
  The name of the generic package.
462
259
  package_version : str
463
260
  The version of the generic package
464
- pfile : str
465
- The relative path of the file that is uploaded. If left empty,
466
- all files from the source folder, and it's subfolders, are uploaded.
467
261
  source : str
468
262
  The source folder that is used as root when uploading. If empty,
469
263
  current working directory is used.
@@ -475,24 +269,21 @@ class Packages:
475
269
  """
476
270
  files = []
477
271
  ret = 1
478
- if filename:
479
- files.append(filename)
480
- else:
481
- filelist = glob(os.path.join(source, "**"), recursive=True)
482
- for item in filelist:
483
- # Only add files, not folders
484
- if os.path.isfile(os.path.join(item)):
485
- # Remove the source folder from the path of the files
486
- 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))
487
278
  for afile in files:
488
- ret = self._upload_file(
279
+ ret = self.upload_file(
489
280
  project_id, package_name, package_version, afile, source
490
281
  )
491
282
  if ret:
492
283
  break
493
284
  return ret
494
285
 
495
- def _upload_file(
286
+ def upload_file(
496
287
  self,
497
288
  project_id: str,
498
289
  package_name: str,
@@ -526,7 +317,7 @@ class Packages:
526
317
  fpath = os.path.join(source, filename)
527
318
  logger.debug("Uploading file %s from %s", filename, source)
528
319
  with open(fpath, "rb") as data:
529
- ret = self.put(
320
+ ret = self.gl_api.put(
530
321
  data.read(),
531
322
  "projects",
532
323
  parse.quote_plus(project_id),
@@ -538,9 +329,7 @@ class Packages:
538
329
  )
539
330
  return ret
540
331
 
541
- def delete_package(
542
- self, project_id: str, package_name: str, package_version: str
543
- ) -> int:
332
+ def delete(self, project_id: str, package_name: str, package_version: str) -> int:
544
333
  """
545
334
  Deletes a version of a GitLab generic package.
546
335
 
@@ -562,7 +351,7 @@ class Packages:
562
351
  ret = 1
563
352
  package_id = self.get_id(project_id, package_name, package_version)
564
353
  if package_id > 0:
565
- ret = self.delete(
354
+ ret = self.gl_api.delete(
566
355
  "projects", parse.quote_plus(project_id), "packages", str(package_id)
567
356
  )
568
357
  return ret
@@ -612,7 +401,7 @@ class Packages:
612
401
  package_id,
613
402
  project_id,
614
403
  )
615
- ret = self.delete(
404
+ ret = self.gl_api.delete(
616
405
  "projects",
617
406
  parse.quote_plus(project_id),
618
407
  "packages",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: glpkg
3
- Version: 1.4.1
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
@@ -25,7 +25,7 @@ Install the tool from with pip:
25
25
  pip install glpkg
26
26
  ```
27
27
 
28
- To check the installation and version, run:
28
+ To check the installation and version, run:
29
29
 
30
30
  ```bash
31
31
  glpkg --version
@@ -35,6 +35,14 @@ If you see a version in the terminal, you're good to go!
35
35
 
36
36
  ## Usage
37
37
 
38
+ When in doubt
39
+
40
+ ```bash
41
+ glpkg --help
42
+ ```
43
+
44
+ might help
45
+
38
46
  By default, the used GitLab host is gitlab.com. If you use a self-hosted GitLab, use argument `--host my-gitlab.net` with the commands.
39
47
 
40
48
  > Only https scheme is supported.
@@ -93,6 +101,29 @@ glpkg download --project 12345 --name mypackagename --version 1.5 --file the_onl
93
101
 
94
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.
95
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
+
96
127
  ### Upload a file to a generic package
97
128
 
98
129
  To upload a file to a version of a generic package, run
@@ -150,7 +181,7 @@ The token that is used to delete packages must have at least Maintainer role in
150
181
 
151
182
  ### Use in GitLab pipelines
152
183
 
153
- 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`.
154
185
 
155
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:
156
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,10 +0,0 @@
1
- gitlab/__init__.py,sha256=1T0uTtX3reNQ2YGbnTeO3F2vueUfxkkK_gkz1OKSZWU,105
2
- gitlab/__main__.py,sha256=ol-xxwX5PONbnqEsq5w-5_bowDyg9Zu-eJZG5XrJeFc,427
3
- gitlab/cli_handler.py,sha256=02Yc3yE3jNMGTx_HvUu0rYH5NwxrnTRaaeAGMFh8Mq8,14273
4
- gitlab/packages.py,sha256=wP0hRLad09LoE9uinUljrvGoBt6KfP49QESqOpxLihQ,20255
5
- glpkg-1.4.1.dist-info/licenses/LICENSE.md,sha256=josGXvZq628dNS0Iru58-DPE7dRpDXzjJxKKT35103g,1065
6
- glpkg-1.4.1.dist-info/METADATA,sha256=aQuiPWniN1eGzhFg37_BBLAvjmp9Xenbq-R0dlcCiMo,7678
7
- glpkg-1.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- glpkg-1.4.1.dist-info/entry_points.txt,sha256=xHPZwx2oShYDZ3AyH7WSIvuhFMssy7QLlQk-JAbje_w,46
9
- glpkg-1.4.1.dist-info/top_level.txt,sha256=MvIaP8p_Oaf4gO_hXmHkX-5y2deHLp1pe6tJR3ukQ6o,7
10
- glpkg-1.4.1.dist-info/RECORD,,