glpkg 1.3.0__py3-none-any.whl → 1.4.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.3.0"
5
+ __version__ = "1.4.0"
gitlab/cli_handler.py CHANGED
@@ -40,6 +40,13 @@ class CLIHandler:
40
40
  name="upload", description="Uploads file to a specific package version."
41
41
  )
42
42
  self._register_upload_parser(upload_parser)
43
+ delete_parser = subparsers.add_parser(
44
+ name="delete",
45
+ description="Deletes a specific package version or a specific file from "
46
+ "a specific package version, depending whether the --file argument is "
47
+ "set or not.",
48
+ )
49
+ self._register_delete_parser(delete_parser)
43
50
  self.args = parser.parse_args()
44
51
 
45
52
  def _print_version(self, _args: argparse.Namespace) -> int:
@@ -240,16 +247,16 @@ class CLIHandler:
240
247
  host, project, name, token_user, token = self._args(args)
241
248
  version = args.version
242
249
  destination = args.destination
243
- gitlab = Packages(host, token_user, token)
244
- package_id = gitlab.get_package_id(project, name, version)
250
+ packages = Packages(host, token_user, token)
251
+ package_id = packages.get_id(project, name, version)
245
252
  if package_id:
246
253
  files = []
247
254
  if args.file:
248
255
  files.append(args.file)
249
256
  else:
250
- files = gitlab.list_files(project, package_id)
257
+ files = packages.get_files(project, package_id).keys()
251
258
  for file in files:
252
- ret = gitlab.download_file(project, name, version, file, destination)
259
+ ret = packages.download_file(project, name, version, file, destination)
253
260
  if ret:
254
261
  print("Failed to download file " + file)
255
262
  break
@@ -287,10 +294,10 @@ class CLIHandler:
287
294
  Zero if everything goes well, non-zero otherwise
288
295
  """
289
296
  host, project, name, token_user, token = self._args(args)
290
- gitlab = Packages(host, token_user, token)
291
- packages = gitlab.list_packages(project, name)
297
+ packages = Packages(host, token_user, token)
298
+ package_list = packages.get_versions(project, name)
292
299
  print("Name" + "\t\t" + "Version")
293
- for package in packages:
300
+ for package in package_list:
294
301
  print(package["name"] + "\t" + package["version"])
295
302
 
296
303
  def _register_upload_parser(self, parser: argparse.ArgumentParser):
@@ -356,6 +363,57 @@ class CLIHandler:
356
363
  print("File " + file + " does not exist!")
357
364
  ret = 1
358
365
  if not ret:
359
- gitlab = Packages(host, token_user, token)
360
- ret = gitlab.upload_file(project, name, version, file, source)
366
+ packages = Packages(host, token_user, token)
367
+ ret = packages.upload_file(project, name, version, file, source)
368
+ return ret
369
+
370
+ def _register_delete_parser(self, parser: argparse.ArgumentParser):
371
+ """
372
+ Registers the delete command related arguments to the parser:
373
+ - version
374
+ - file
375
+ - action
376
+
377
+ Additionally, registers the common args.
378
+
379
+ Parameters
380
+ ----------
381
+ parser:
382
+ The argparser where to register the upload arguments.
383
+ """
384
+ self._register_common_arguments(parser)
385
+ parser.add_argument("-v", "--version", type=str, help="The package version.")
386
+ parser.add_argument(
387
+ "-f",
388
+ "--file",
389
+ type=str,
390
+ help="The file to be deleted, for example my_file.txt. Note that "
391
+ "only relative paths (to the package root) are supported. If undefined, "
392
+ "the package version is deleted.",
393
+ )
394
+ parser.set_defaults(action=self._delete)
395
+
396
+ def _delete(self, args: argparse.Namespace) -> int:
397
+ """
398
+ Deletes a file from a GitLab generic package
399
+
400
+ Parameters
401
+ ----------
402
+ args : argparse.Namespace
403
+ The arguments from command line
404
+
405
+ Returns
406
+ -------
407
+ int
408
+ Zero if everything went fine, non-zero otherwise.
409
+ """
410
+ ret = 0
411
+ host, project, name, token_user, token = self._args(args)
412
+ version = args.version
413
+ file = args.file
414
+ packages = Packages(host, token_user, token)
415
+ if file:
416
+ ret = packages.delete_file(project, name, version, file)
417
+ else:
418
+ ret = packages.delete_package(project, name, version)
361
419
  return ret
gitlab/packages.py CHANGED
@@ -15,7 +15,7 @@ class Packages:
15
15
 
16
16
  def __init__(self, host: str, token_type: str, token: str):
17
17
  """
18
- Creates a new instance of Packages class.
18
+ Creates a new instance of class.
19
19
 
20
20
  Parameters
21
21
  ----------
@@ -37,7 +37,7 @@ class Packages:
37
37
  self.token_type = token_type
38
38
  self.token = token
39
39
 
40
- def api_url(self) -> str:
40
+ def _url(self) -> str:
41
41
  """
42
42
  Returns the GitLab REST API URL by using the host variable.
43
43
 
@@ -48,25 +48,7 @@ class Packages:
48
48
  """
49
49
  return f"https://{self.host}/api/v4/"
50
50
 
51
- def project_api_url(self, project: str) -> str:
52
- """
53
- Returns the project REST API URL of the project
54
-
55
- Parameters
56
- ----------
57
- project : str
58
- The project ID or the path of the project, including namespace.
59
- Examples: `123` or `namespace/project`.
60
-
61
- Returns
62
- -------
63
- str
64
- The project REST API URL, for example `https://gitlab.com/api/v4/projects/123/`
65
- or `https://gitlab.com/api/v4/projects/namespace%2Fproject/`
66
- """
67
- return f"{self.api_url()}projects/{parse.quote_plus(project)}/"
68
-
69
- def get_headers(self) -> dict:
51
+ def _get_headers(self) -> dict:
70
52
  """
71
53
  Creates headers for a GitLab REST API call.
72
54
 
@@ -83,98 +65,145 @@ class Packages:
83
65
  headers = {self.token_type: self.token}
84
66
  return headers
85
67
 
86
- def _request(self, url: str) -> tuple[int, bytes, HTTPMessage]:
68
+ def _parse_header_links(self, headers: HTTPMessage) -> dict:
87
69
  """
88
- Makes a HTTP request to the given URL, and returns
89
- the response status, body, and headers.
70
+ Parses link field from HTTP headers to a dictionary where
71
+ the link "rel" value is the key.
90
72
 
73
+ This is useful for example with GitLab REST API to get the pagination links.
91
74
 
92
75
  Parameters
93
76
  ----------
94
- url : str
95
- The URL of the HTTP request to make.
77
+ headers : HTTPMessage
78
+ The HTTP response headers that contain the links
96
79
 
97
80
  Returns
98
81
  -------
99
- int
100
- The HTTP response code, such as 200
101
- bytes
102
- The HTTP response body read as bytes
103
- HTTPMessage
104
- The HTTP response headers
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.
105
85
  """
106
- logger.debug("Requesting %s", url)
107
- req = request.Request(url, headers=self.get_headers())
108
- with request.urlopen(req) as response:
109
- return response.status, response.read(), response.headers
110
-
111
- def _get_next_page(self, headers: HTTPMessage) -> int:
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:
112
104
  """
113
- Returns the next page from headers for pagination.
114
-
115
- Uses the field x-next-page from the headers.
105
+ Returns the path to be appended to the REST API URL
116
106
 
117
107
  Parameters
118
108
  ----------
119
- headers : HTTPMessage
120
- The header from which to get the next page number for
121
- pagination
109
+ paths : str
110
+ The path string that are joined with "/".
122
111
 
123
112
  Returns
124
113
  -------
125
- int
126
- The next page number. If headers were empty or they
127
- did not include suitable item, returns 0. In such
128
- case, do not attempt to fetch a next page.
129
- """
130
- ret = 0
131
- if headers:
132
- next_page = headers.get("x-next-page")
133
- if next_page:
134
- ret = int(next_page)
135
- logger.debug("Response incomplete, next page is %s", next_page)
136
- else:
137
- logger.debug("Response complete")
138
- return ret
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)
139
122
 
140
- def _build_query(self, arg: str, page: int) -> str:
123
+ def build_query(self, **args: str) -> str:
141
124
  """
142
125
  Builds a query for a GitLab REST API request
143
126
 
144
127
  Parameters
145
128
  ----------
146
- arg : str
147
- The args of the query that is endpoint specific
148
- page : int
149
- Page number for the pagination of the request.
150
- Set to 0 to omit the pagination.
129
+ args : str
130
+ keyword arguments for the REST API query, like per_page=20, page=20
151
131
 
152
132
  Returns
153
133
  -------
154
134
  str
155
135
  A query string for a REST API request. Append this to
156
- the request URL. Example `?arg=this&page=3`.
136
+ the request URL. Example `?per_page=20&page=20`.
157
137
  """
158
138
  query = ""
159
- if arg or page:
160
- if page:
161
- page = "page=" + str(page)
162
- query = f"?{'&'.join(filter(None, (arg, page)))}"
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
163
143
  return query
164
144
 
165
- def gl_project_api(self, project: str, apath: str, arg: str = None) -> list:
145
+ def _get(self, url: str, headers: dict) -> tuple[int, bytes, HTTPMessage]:
166
146
  """
167
- Returns data from the project REST API for the path. In case
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
168
199
  of multiple pages, all data will be returned.
169
200
 
170
201
  Parameters
171
202
  ----------
172
- project : str
173
- The project ID or the path of the project, including namespace.
174
- Examples: `123` or `namespace/project`.
175
- apath : str
176
- The path of the project API endpoint that is called. For example packages
177
- arg : str, optional
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
178
207
  Additional arguments for the query of the URL, for example to filter
179
208
  results: package_name=mypackage
180
209
 
@@ -183,31 +212,80 @@ class Packages:
183
212
  list
184
213
  Data from GitLab REST API endpoint with the arguments.
185
214
  """
215
+ url = self._url() + self._build_path(*paths) + self.build_query(**query_params)
186
216
  data = []
187
- more = True
188
- page = None
189
- while more:
190
- more = False
191
- query = self._build_query(arg, page)
192
- url = self.project_api_url(project) + apath + query
193
- status, res_data, headers = self._request(url)
194
- logger.debug("Response status: %d", status)
217
+ while url:
218
+ res_status, res_data, res_headers = self._get(url, self._get_headers())
219
+ logger.debug("Response status: %d", res_status)
195
220
  res_data = loads(res_data)
196
221
  logger.debug("Response data: %s", res_data)
197
222
  data = data + res_data
198
- page = self._get_next_page(headers)
199
- if page:
200
- more = True
223
+ url = self._parse_header_links(res_headers).get("next")
201
224
  return data
202
225
 
203
- def list_packages(self, project: str, package_name: str) -> list:
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
280
+
281
+ def get_versions(self, project_id: str, package_name: str) -> list:
204
282
  """
205
283
  Lists the available versions of the package
206
284
 
207
285
  Parameters
208
286
  ----------
209
- project : str
210
- The project ID or the path of the project, including namespace.
287
+ project_id : str
288
+ The project ID or path, including namespace.
211
289
  Examples: `123` or `namespace/project`.
212
290
  package_name : str
213
291
  The name of the package that is listed.
@@ -219,8 +297,12 @@ class Packages:
219
297
  """
220
298
  packages = []
221
299
  logger.debug("Listing packages with name %s", package_name)
222
- data = self.gl_project_api(
223
- project, "packages", "package_name=" + parse.quote_plus(package_name)
300
+ data = self.get_all(
301
+ "projects",
302
+ parse.quote_plus(project_id),
303
+ "packages",
304
+ package_name=package_name,
305
+ package_type="generic",
224
306
  )
225
307
  for package in data:
226
308
  name = parse.unquote(package["name"])
@@ -232,46 +314,52 @@ class Packages:
232
314
  packages.append({"name": name, "version": version})
233
315
  return packages
234
316
 
235
- def list_files(self, project: str, package_id: int) -> list:
317
+ def get_files(self, project_id: str, package_id: int) -> dict:
236
318
  """
237
319
  Lists all files of a specific package ID from GitLab REST API
238
320
 
239
321
  Parameters
240
322
  ----------
241
- project : str
242
- The project ID or the path of the project, including namespace.
323
+ project_id : str
324
+ The project ID or path, including namespace.
243
325
  Examples: `123` or `namespace/project`.
244
326
  package_id : int
245
327
  The package ID that is listed
246
328
 
247
329
  Return
248
330
  ------
249
- list
250
- List of file (names) that are in the package.
331
+ dict
332
+ Dictionary of file (names) that are in the package, with
333
+ each element containing a dictionary containing information
334
+ of the file
251
335
  """
252
- files = []
336
+ files = {}
253
337
  logger.debug("Listing package %d files", package_id)
254
- apath = "packages/" + parse.quote_plus(str(package_id)) + "/package_files"
255
- data = self.gl_project_api(project, apath)
338
+ data = self.get_all(
339
+ "projects",
340
+ parse.quote_plus(project_id),
341
+ "packages",
342
+ str(package_id),
343
+ "package_files",
344
+ )
256
345
  for package in data:
257
346
  # Only append the filename once to the list of files
258
347
  # as there's no way to download them separately through
259
348
  # the API
260
349
  filename = parse.unquote(package["file_name"])
261
- if filename not in files:
262
- files.append(filename)
350
+ file_id = package["id"]
351
+ if not files.get(filename):
352
+ files[filename] = {"id": file_id}
263
353
  return files
264
354
 
265
- def get_package_id(
266
- self, project: str, package_name: str, package_version: str
267
- ) -> int:
355
+ def get_id(self, project_id: str, package_name: str, package_version: str) -> int:
268
356
  """
269
357
  Gets the package ID of a specific package version.
270
358
 
271
359
  Parameters
272
360
  ----------
273
- project : str
274
- The project ID or the path of the project, including namespace.
361
+ project_id : str
362
+ The project ID or path, including namespace.
275
363
  Examples: `123` or `namespace/project`.
276
364
  package_name : str
277
365
  The name of the package.
@@ -281,18 +369,18 @@ class Packages:
281
369
  Return
282
370
  ------
283
371
  int
284
- The ID of the package. Zero if no ID was found.
372
+ The ID of the package. -1 if no ID was found.
285
373
  """
286
- package_id = 0
374
+ package_id = -1
287
375
  logger.debug("Fetching package %s (%s) ID", package_name, package_version)
288
- apath = "packages"
289
- arg = (
290
- "package_name="
291
- + parse.quote_plus(package_name)
292
- + "&package_version="
293
- + parse.quote_plus(package_version)
376
+ data = self.get_all(
377
+ "projects",
378
+ parse.quote_plus(project_id),
379
+ "packages",
380
+ package_name=package_name,
381
+ package_version=package_version,
382
+ package_type="generic",
294
383
  )
295
- data = self.gl_project_api(project, apath, arg)
296
384
  if len(data) == 1:
297
385
  package = data.pop()
298
386
  package_id = package["id"]
@@ -300,7 +388,7 @@ class Packages:
300
388
 
301
389
  def download_file(
302
390
  self,
303
- project: str,
391
+ project_id: str,
304
392
  package_name: str,
305
393
  package_version: str,
306
394
  filename: str,
@@ -311,8 +399,8 @@ class Packages:
311
399
 
312
400
  Parameters
313
401
  ----------
314
- project : str
315
- The project ID or the path of the project, including namespace.
402
+ project_id : str
403
+ The project ID or path, including namespace.
316
404
  Examples: `123` or `namespace/project`.
317
405
  package_name : str
318
406
  The name of the generic package.
@@ -331,16 +419,15 @@ class Packages:
331
419
  """
332
420
  ret = 1
333
421
  logger.debug("Downloading file %s", filename)
334
- url = (
335
- self.project_api_url(project)
336
- + "packages/generic/"
337
- + parse.quote_plus(package_name)
338
- + "/"
339
- + parse.quote_plus(package_version)
340
- + "/"
341
- + parse.quote(filename)
422
+ status, data, _ = self.get(
423
+ "projects",
424
+ parse.quote_plus(project_id),
425
+ "packages",
426
+ "generic",
427
+ parse.quote_plus(package_name),
428
+ parse.quote_plus(package_version),
429
+ parse.quote(filename),
342
430
  )
343
- status, data, _ = self._request(url)
344
431
  if status == 200:
345
432
  fpath = os.path.join(destination, filename)
346
433
  parent = os.path.dirname(fpath)
@@ -356,10 +443,10 @@ class Packages:
356
443
 
357
444
  def upload_file(
358
445
  self,
359
- project: str,
446
+ project_id: str,
360
447
  package_name: str,
361
448
  package_version: str,
362
- pfile: str,
449
+ filename: str,
363
450
  source: str,
364
451
  ) -> int:
365
452
  """
@@ -367,14 +454,14 @@ class Packages:
367
454
 
368
455
  Parameters
369
456
  ----------
370
- project : str
371
- The project ID or the path of the project, including namespace.
457
+ project_id : str
458
+ The project ID or path, including namespace.
372
459
  Examples: `123` or `namespace/project`.
373
460
  package_name : str
374
461
  The name of the generic package.
375
462
  package_version : str
376
463
  The version of the generic package
377
- file : str
464
+ pfile : str
378
465
  The relative path of the file that is uploaded. If left empty,
379
466
  all files from the source folder, and it's subfolders, are uploaded.
380
467
  source : str
@@ -388,8 +475,8 @@ class Packages:
388
475
  """
389
476
  files = []
390
477
  ret = 1
391
- if pfile:
392
- files.append(pfile)
478
+ if filename:
479
+ files.append(filename)
393
480
  else:
394
481
  filelist = glob(os.path.join(source, "**"), recursive=True)
395
482
  for item in filelist:
@@ -397,9 +484,9 @@ class Packages:
397
484
  if os.path.isfile(os.path.join(item)):
398
485
  # Remove the source folder from the path of the files
399
486
  files.append(os.path.relpath(item, source))
400
- for ufile in files:
487
+ for afile in files:
401
488
  ret = self._upload_file(
402
- project, package_name, package_version, ufile, source
489
+ project_id, package_name, package_version, afile, source
403
490
  )
404
491
  if ret:
405
492
  break
@@ -407,10 +494,10 @@ class Packages:
407
494
 
408
495
  def _upload_file(
409
496
  self,
410
- project: str,
497
+ project_id: str,
411
498
  package_name: str,
412
499
  package_version: str,
413
- pfile: str,
500
+ filename: str,
414
501
  source: str,
415
502
  ) -> int:
416
503
  """
@@ -418,14 +505,14 @@ class Packages:
418
505
 
419
506
  Parameters
420
507
  ----------
421
- project : str
422
- The project ID or the path of the project, including namespace.
508
+ project_id : str
509
+ The project ID or path, including namespace.
423
510
  Examples: `123` or `namespace/project`.
424
511
  package_name : str
425
512
  The name of the generic package.
426
513
  package_version : str
427
514
  The version of the generic package
428
- file : str
515
+ filename : str
429
516
  The relative path of the file that is uploaded.
430
517
  source : str
431
518
  The source folder that is used as root when uploading.
@@ -436,22 +523,101 @@ class Packages:
436
523
  Zero if everything went fine, non-zero coke otherwise.
437
524
  """
438
525
  ret = 1
439
- fpath = os.path.join(source, pfile)
440
- logger.debug("Uploading file %s from %s", pfile, source)
526
+ fpath = os.path.join(source, filename)
527
+ logger.debug("Uploading file %s from %s", filename, source)
441
528
  with open(fpath, "rb") as data:
442
- url = (
443
- self.project_api_url(project)
444
- + "packages/generic/"
445
- + parse.quote_plus(package_name)
446
- + "/"
447
- + parse.quote_plus(package_version)
448
- + "/"
449
- + parse.quote(pfile)
529
+ ret = self.put(
530
+ data.read(),
531
+ "projects",
532
+ parse.quote_plus(project_id),
533
+ "packages",
534
+ "generic",
535
+ parse.quote_plus(package_name),
536
+ parse.quote_plus(package_version),
537
+ parse.quote(filename),
450
538
  )
451
- req = request.Request(
452
- url, method="PUT", data=data, headers=self.get_headers()
539
+ return ret
540
+
541
+ def delete_package(
542
+ self, project_id: str, package_name: str, package_version: str
543
+ ) -> int:
544
+ """
545
+ Deletes a version of a GitLab generic package.
546
+
547
+ Parameters
548
+ ----------
549
+ project_id : str
550
+ The project ID or path, including namespace.
551
+ Examples: `123` or `namespace/project`.
552
+ package_name : str
553
+ The name of the generic package.
554
+ package_version : str
555
+ The version of the generic package that is deleted
556
+
557
+ Return
558
+ ------
559
+ int
560
+ Zero if everything went fine, non-zero coke otherwise.
561
+ """
562
+ ret = 1
563
+ package_id = self.get_id(project_id, package_name, package_version)
564
+ if package_id > 0:
565
+ ret = self.delete(
566
+ "projects", parse.quote_plus(project_id), "packages", str(package_id)
453
567
  )
454
- with request.urlopen(req) as res:
455
- if res.status == 201: # 201 is created
456
- ret = 0
568
+ return ret
569
+
570
+ def delete_file(
571
+ self,
572
+ project_id: str,
573
+ package_name: str,
574
+ package_version: str,
575
+ filename: str,
576
+ ) -> int:
577
+ """
578
+ Deletes a file from a GitLab generic package.
579
+
580
+ Parameters
581
+ ----------
582
+ project_id : str
583
+ The project ID or path, including namespace.
584
+ Examples: `123` or `namespace/project`.
585
+ package_name : str
586
+ The name of the generic package.
587
+ package_version : str
588
+ The version of the generic package
589
+ filename : str
590
+ The path of the file to be deleted in the package.
591
+
592
+ Return
593
+ ------
594
+ int
595
+ Zero if everything went fine, non-zero coke otherwise.
596
+ """
597
+ ret = 1
598
+ package_id = self.get_id(project_id, package_name, package_version)
599
+ if package_id > 0:
600
+ package_files = self.get_files(project_id, package_id)
601
+ file_id = package_files.get(filename)
602
+ if file_id and file_id.get("id"):
603
+ file_id = file_id.get("id")
604
+ ret = self._delete_file(project_id, package_id, file_id)
605
+ return ret
606
+
607
+ def _delete_file(self, project_id: str, package_id: int, file_id: int) -> int:
608
+ ret = 1
609
+ logger.info(
610
+ "Deleting file %d from package %d from project %s",
611
+ file_id,
612
+ package_id,
613
+ project_id,
614
+ )
615
+ ret = self.delete(
616
+ "projects",
617
+ parse.quote_plus(project_id),
618
+ "packages",
619
+ str(package_id),
620
+ "package_files",
621
+ str(file_id),
622
+ )
457
623
  return ret
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: glpkg
3
- Version: 1.3.0
3
+ Version: 1.4.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
@@ -15,6 +15,7 @@ Dynamic: license-file
15
15
 
16
16
  glpkg is a tool that makes it easy to work with [GitLab generic packages](https://docs.gitlab.com/user/packages/generic_packages/).
17
17
 
18
+ Generic package registries of GitLab projects are supported. Group registries are not, as the GitLab REST API for groups is very limited.
18
19
 
19
20
  ## Installation
20
21
 
@@ -26,7 +27,6 @@ pip install glpkg
26
27
 
27
28
  To check the installation and version, run:
28
29
 
29
-
30
30
  ```bash
31
31
  glpkg --version
32
32
  ```
@@ -117,6 +117,37 @@ glpkg upload --project 12345 --name mypackagename --version 1.0 --source upload
117
117
 
118
118
  Note: a file in `upload` folder will be uploaded to the root of the package. If you want to upload the file to a `dir` folder in the package, make a structure in the upload folder, like `upload/dir/`.
119
119
 
120
+ ### Delete a file from a generic package
121
+
122
+ To delete a file from a specific generic package, run
123
+
124
+ ```bash
125
+ glpkg delete --project 12345 --name mypackagename --version 1.0 --file my-file.txt
126
+ ```
127
+
128
+ Where
129
+ - `12345` is your projects ID ([Find the Project ID](https://docs.gitlab.com/user/project/working_with_projects/#find-the-project-id)) or the path of the project (like `namespace/project`)
130
+ - `mypackagename` is the name of the generic package
131
+ - `1.0` is the version of the generic package from the file is deleted
132
+ - `my-file.txt` is the file that is deleted. Only relative paths are supported. Note that the package may contain multiple files with same name. In this case, one file of them is deleted.
133
+
134
+ The token that is used to delete files must have at least Maintainer role in the project.
135
+
136
+ ### Delete a package version
137
+
138
+ To delete a specific generic package version, run
139
+
140
+ ```bash
141
+ glpkg delete --project 12345 --name mypackagename --version 1.0
142
+ ```
143
+
144
+ Where
145
+ - `12345` is your projects ID ([Find the Project ID](https://docs.gitlab.com/user/project/working_with_projects/#find-the-project-id)) or the path of the project (like `namespace/project`)
146
+ - `mypackagename` is the name of the generic package
147
+ - `1.0` is the version of the generic package that is deleted
148
+
149
+ The token that is used to delete packages must have at least Maintainer role in the project.
150
+
120
151
  ### Use in GitLab pipelines
121
152
 
122
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`.
@@ -128,10 +159,3 @@ glpkg upload --ci --name mypackagename --version 1.0 --file my-file.txt
128
159
  ```
129
160
 
130
161
  To use the `CI_JOB_TOKEN` with package registry of another projects, add `--project <otherproject ID>` argument. Remember that you may need to add [permissions for the CI_JOB_TOKEN](https://docs.gitlab.com/ci/jobs/ci_job_token/#control-job-token-access-to-your-project) in the other project.
131
-
132
-
133
- ## Limitations
134
-
135
- The tool is not perfect (yet) and has limitations. The following limitations are known, but more can exist:
136
-
137
- - Only project registries are supported for now.
@@ -0,0 +1,10 @@
1
+ gitlab/__init__.py,sha256=TXqR34k7YgbFTeeKfSOdxBH0lFaC3-SiZU5mTBL9GI0,105
2
+ gitlab/__main__.py,sha256=UPVixjP98KzJi9Ljth4yqqDTTaE3EfyAuj2xGCQAGys,586
3
+ gitlab/cli_handler.py,sha256=02Yc3yE3jNMGTx_HvUu0rYH5NwxrnTRaaeAGMFh8Mq8,14273
4
+ gitlab/packages.py,sha256=wP0hRLad09LoE9uinUljrvGoBt6KfP49QESqOpxLihQ,20255
5
+ glpkg-1.4.0.dist-info/licenses/LICENSE.md,sha256=josGXvZq628dNS0Iru58-DPE7dRpDXzjJxKKT35103g,1065
6
+ glpkg-1.4.0.dist-info/METADATA,sha256=PX4jWVi_HCznrjQ9OztSYqYurdqv1-qHXKbkRiIUVlU,7678
7
+ glpkg-1.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ glpkg-1.4.0.dist-info/entry_points.txt,sha256=xHPZwx2oShYDZ3AyH7WSIvuhFMssy7QLlQk-JAbje_w,46
9
+ glpkg-1.4.0.dist-info/top_level.txt,sha256=MvIaP8p_Oaf4gO_hXmHkX-5y2deHLp1pe6tJR3ukQ6o,7
10
+ glpkg-1.4.0.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- gitlab/__init__.py,sha256=ZXRwtUQEDE9bdewmwKYCjMAhm5KsuvAe525_QBiWLHY,105
2
- gitlab/__main__.py,sha256=UPVixjP98KzJi9Ljth4yqqDTTaE3EfyAuj2xGCQAGys,586
3
- gitlab/cli_handler.py,sha256=2P3R-yYkT6D5CjNCAEd9FC9HYI09aJ_eyMSMsOHtZzs,12289
4
- gitlab/packages.py,sha256=50CbLdYxj0r6l8J40TxpIo2hTkKlv1BCFywFR6lg2nc,14785
5
- glpkg-1.3.0.dist-info/licenses/LICENSE.md,sha256=josGXvZq628dNS0Iru58-DPE7dRpDXzjJxKKT35103g,1065
6
- glpkg-1.3.0.dist-info/METADATA,sha256=4bQDKmHrf2da0eQ2hjfQKKItoirfcIr1VbWa3hwxbwY,6362
7
- glpkg-1.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- glpkg-1.3.0.dist-info/entry_points.txt,sha256=xHPZwx2oShYDZ3AyH7WSIvuhFMssy7QLlQk-JAbje_w,46
9
- glpkg-1.3.0.dist-info/top_level.txt,sha256=MvIaP8p_Oaf4gO_hXmHkX-5y2deHLp1pe6tJR3ukQ6o,7
10
- glpkg-1.3.0.dist-info/RECORD,,
File without changes