glpkg 1.2.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 +3 -1
- gitlab/__main__.py +17 -0
- gitlab/cli_handler.py +267 -31
- gitlab/packages.py +546 -99
- {glpkg-1.2.0.dist-info → glpkg-1.4.0.dist-info}/METADATA +42 -11
- glpkg-1.4.0.dist-info/RECORD +10 -0
- glpkg-1.2.0.dist-info/RECORD +0 -10
- {glpkg-1.2.0.dist-info → glpkg-1.4.0.dist-info}/WHEEL +0 -0
- {glpkg-1.2.0.dist-info → glpkg-1.4.0.dist-info}/entry_points.txt +0 -0
- {glpkg-1.2.0.dist-info → glpkg-1.4.0.dist-info}/licenses/LICENSE.md +0 -0
- {glpkg-1.2.0.dist-info → glpkg-1.4.0.dist-info}/top_level.txt +0 -0
gitlab/__init__.py
CHANGED
gitlab/__main__.py
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
|
+
"""GitLab packages main module"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
1
4
|
import sys
|
|
2
5
|
|
|
3
6
|
from gitlab.cli_handler import CLIHandler
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
def cli() -> int:
|
|
10
|
+
"""
|
|
11
|
+
Runs the main program of the glpkg.
|
|
12
|
+
|
|
13
|
+
Uses arguments from command line and executes the given command.
|
|
14
|
+
|
|
15
|
+
Return
|
|
16
|
+
------
|
|
17
|
+
int
|
|
18
|
+
Zero when everything is fine, non-zero otherwise.
|
|
19
|
+
"""
|
|
20
|
+
logging.basicConfig(
|
|
21
|
+
level=logging.DEBUG,
|
|
22
|
+
handlers=[logging.FileHandler("glpkg.log")], # , logging.StreamHandler()],
|
|
23
|
+
)
|
|
7
24
|
handler = CLIHandler()
|
|
8
25
|
return handler.do_it()
|
|
9
26
|
|
gitlab/cli_handler.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
"""CLI handler for glpkg"""
|
|
2
|
+
|
|
1
3
|
import argparse
|
|
2
|
-
import netrc
|
|
4
|
+
from netrc import netrc
|
|
3
5
|
import os
|
|
4
6
|
import sys
|
|
5
7
|
import urllib
|
|
@@ -7,7 +9,15 @@ from gitlab import Packages, __version__
|
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class CLIHandler:
|
|
12
|
+
"""Class to parse CLI arguments and run the requested command"""
|
|
13
|
+
|
|
10
14
|
def __init__(self):
|
|
15
|
+
"""
|
|
16
|
+
Creates a new instance of CLIHandler.
|
|
17
|
+
|
|
18
|
+
Parses the arguments from command line and prepares everything
|
|
19
|
+
to be ready to executed. Just use do_it to run it!
|
|
20
|
+
"""
|
|
11
21
|
parser = argparse.ArgumentParser(
|
|
12
22
|
description="Toolbox for GitLab generic packages"
|
|
13
23
|
)
|
|
@@ -16,25 +26,57 @@ class CLIHandler:
|
|
|
16
26
|
subparsers = parser.add_subparsers()
|
|
17
27
|
list_parser = subparsers.add_parser(
|
|
18
28
|
name="list",
|
|
19
|
-
description="Lists the available version of a package from the
|
|
29
|
+
description="Lists the available version of a package from the "
|
|
30
|
+
"package registry.",
|
|
20
31
|
)
|
|
21
32
|
self._register_list_parser(list_parser)
|
|
22
33
|
download_parser = subparsers.add_parser(
|
|
23
34
|
name="download",
|
|
24
|
-
description="Downloads all files from a specific package version
|
|
35
|
+
description="Downloads all files from a specific package version "
|
|
36
|
+
"to the current directory.",
|
|
25
37
|
)
|
|
26
38
|
self._register_download_parser(download_parser)
|
|
27
39
|
upload_parser = subparsers.add_parser(
|
|
28
40
|
name="upload", description="Uploads file to a specific package version."
|
|
29
41
|
)
|
|
30
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)
|
|
31
50
|
self.args = parser.parse_args()
|
|
32
51
|
|
|
33
|
-
def _print_version(self,
|
|
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
|
+
"""
|
|
34
66
|
print(__version__)
|
|
35
67
|
return 0
|
|
36
68
|
|
|
37
69
|
def do_it(self) -> int:
|
|
70
|
+
"""
|
|
71
|
+
Executes the requested command.
|
|
72
|
+
|
|
73
|
+
In case of error, prints to stderr.
|
|
74
|
+
|
|
75
|
+
Return
|
|
76
|
+
------
|
|
77
|
+
int
|
|
78
|
+
Zero when everything went fine, non-zero otherwise.
|
|
79
|
+
"""
|
|
38
80
|
ret = 1
|
|
39
81
|
try:
|
|
40
82
|
ret = self.args.action(self.args)
|
|
@@ -50,20 +92,38 @@ class CLIHandler:
|
|
|
50
92
|
print("Check your arguments and credentials.", file=sys.stderr)
|
|
51
93
|
return ret
|
|
52
94
|
|
|
53
|
-
def _register_common_arguments(self, parser) -> None:
|
|
95
|
+
def _register_common_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Registers common arguments to the parser:
|
|
98
|
+
- host
|
|
99
|
+
- ci
|
|
100
|
+
- project
|
|
101
|
+
- name
|
|
102
|
+
- token
|
|
103
|
+
- netrc
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
parser:
|
|
108
|
+
The argparser where to register the common arguments.
|
|
109
|
+
"""
|
|
54
110
|
group = parser.add_mutually_exclusive_group()
|
|
55
111
|
group.add_argument(
|
|
56
112
|
"-H",
|
|
57
113
|
"--host",
|
|
58
114
|
default="gitlab.com",
|
|
59
115
|
type=str,
|
|
60
|
-
help="The host address of GitLab instance without scheme,
|
|
116
|
+
help="The host address of GitLab instance without scheme, "
|
|
117
|
+
"for example gitlab.com. Note that only https scheme is supported.",
|
|
61
118
|
)
|
|
62
119
|
group.add_argument(
|
|
63
120
|
"-c",
|
|
64
121
|
"--ci",
|
|
65
122
|
action="store_true",
|
|
66
|
-
help="Use this in GitLab jobs. In this case CI_SERVER_HOST, CI_PROJECT_ID,
|
|
123
|
+
help="Use this in GitLab jobs. In this case CI_SERVER_HOST, CI_PROJECT_ID, "
|
|
124
|
+
"and CI_JOB_TOKEN variables from the environment are used. --project and --token "
|
|
125
|
+
"can be used to override project ID and the CI_JOB_TOKEN to a personal or project "
|
|
126
|
+
"access token.",
|
|
67
127
|
)
|
|
68
128
|
parser.add_argument(
|
|
69
129
|
"-p",
|
|
@@ -77,33 +137,77 @@ class CLIHandler:
|
|
|
77
137
|
"-t",
|
|
78
138
|
"--token",
|
|
79
139
|
type=str,
|
|
80
|
-
help="Private or project access token that is used to authenticate with
|
|
140
|
+
help="Private or project access token that is used to authenticate with "
|
|
141
|
+
"the package registry. Leave empty if the registry is public. The token "
|
|
142
|
+
"must have 'read API' or 'API' scope.",
|
|
81
143
|
)
|
|
82
144
|
group2.add_argument(
|
|
83
145
|
"--netrc",
|
|
84
146
|
action="store_true",
|
|
85
|
-
help="Set to use a token from .netrc file (~/.netrc) for the host. The
|
|
147
|
+
help="Set to use a token from .netrc file (~/.netrc) for the host. The "
|
|
148
|
+
".netrc username is ignored due to API restrictions. PRIVATE-TOKEN is used "
|
|
149
|
+
"instead. Note that .netrc file access rights must be correct.",
|
|
86
150
|
)
|
|
87
151
|
|
|
88
|
-
def _register_download_parser(self, parser):
|
|
152
|
+
def _register_download_parser(self, parser: argparse.ArgumentParser):
|
|
153
|
+
"""
|
|
154
|
+
Registers the download command related arguments to the parser:
|
|
155
|
+
- version
|
|
156
|
+
- file
|
|
157
|
+
- destination
|
|
158
|
+
- action
|
|
159
|
+
|
|
160
|
+
Additionally, registers the common args.
|
|
161
|
+
|
|
162
|
+
Parameters
|
|
163
|
+
----------
|
|
164
|
+
parser:
|
|
165
|
+
The argparser where to register the download arguments.
|
|
166
|
+
"""
|
|
89
167
|
self._register_common_arguments(parser)
|
|
90
168
|
parser.add_argument("-v", "--version", type=str, help="The package version.")
|
|
91
169
|
parser.add_argument(
|
|
92
170
|
"-f",
|
|
93
171
|
"--file",
|
|
94
172
|
type=str,
|
|
95
|
-
help="The file to download from the package. If not defined, all
|
|
173
|
+
help="The file to download from the package. If not defined, all "
|
|
174
|
+
"files are downloaded.",
|
|
96
175
|
)
|
|
97
176
|
parser.add_argument(
|
|
98
177
|
"-d",
|
|
99
178
|
"--destination",
|
|
100
179
|
default="",
|
|
101
180
|
type=str,
|
|
102
|
-
help="The path where the file(s) are downloaded. If not defined,
|
|
181
|
+
help="The path where the file(s) are downloaded. If not defined, "
|
|
182
|
+
"the current working directory is used.",
|
|
103
183
|
)
|
|
104
184
|
parser.set_defaults(action=self._download_handler)
|
|
105
185
|
|
|
106
|
-
def _args(self, args):
|
|
186
|
+
def _args(self, args) -> tuple[str, str, str, str, str]:
|
|
187
|
+
"""
|
|
188
|
+
Returns the connection parameters according to the args
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
args:
|
|
193
|
+
The args that are used to determined the connection parameters
|
|
194
|
+
|
|
195
|
+
Returns
|
|
196
|
+
-------
|
|
197
|
+
host : str
|
|
198
|
+
The GitLab host name
|
|
199
|
+
project : str
|
|
200
|
+
The Project ID or name to use
|
|
201
|
+
name : str
|
|
202
|
+
The package name
|
|
203
|
+
token_user : str
|
|
204
|
+
The token user according to the args. If ci is used, returns `JOB-TOKEN`, else
|
|
205
|
+
`PRIVATE-TOKEN`.
|
|
206
|
+
token : str
|
|
207
|
+
The token according to the args. If token is set, returns it. If netrc is set,
|
|
208
|
+
reads the token from the .netrc file. If ci is set, reads the environment
|
|
209
|
+
variable CI_JOB_TOKEN. Otherwise returns None.
|
|
210
|
+
"""
|
|
107
211
|
if args.ci:
|
|
108
212
|
host = os.environ["CI_SERVER_HOST"]
|
|
109
213
|
project = os.environ["CI_PROJECT_ID"]
|
|
@@ -120,26 +224,39 @@ class CLIHandler:
|
|
|
120
224
|
token = args.token
|
|
121
225
|
token_user = "PRIVATE-TOKEN"
|
|
122
226
|
if args.netrc:
|
|
123
|
-
_, _, token = netrc
|
|
227
|
+
_, _, token = netrc().authenticators(host)
|
|
124
228
|
token_user = "PRIVATE-TOKEN"
|
|
125
229
|
name = args.name
|
|
126
230
|
return host, project, name, token_user, token
|
|
127
231
|
|
|
128
|
-
def _download_handler(self, args) -> int:
|
|
232
|
+
def _download_handler(self, args: argparse.Namespace) -> int:
|
|
233
|
+
"""
|
|
234
|
+
Downloads package file(s) from GitLab package registry.
|
|
235
|
+
|
|
236
|
+
Parameters
|
|
237
|
+
----------
|
|
238
|
+
args : argparse.Namespace
|
|
239
|
+
The parsed arguments
|
|
240
|
+
|
|
241
|
+
Returns
|
|
242
|
+
-------
|
|
243
|
+
int
|
|
244
|
+
Zero if everything goes well, non-zero otherwise
|
|
245
|
+
"""
|
|
129
246
|
ret = 1
|
|
130
247
|
host, project, name, token_user, token = self._args(args)
|
|
131
248
|
version = args.version
|
|
132
249
|
destination = args.destination
|
|
133
|
-
|
|
134
|
-
package_id =
|
|
250
|
+
packages = Packages(host, token_user, token)
|
|
251
|
+
package_id = packages.get_id(project, name, version)
|
|
135
252
|
if package_id:
|
|
136
253
|
files = []
|
|
137
254
|
if args.file:
|
|
138
255
|
files.append(args.file)
|
|
139
256
|
else:
|
|
140
|
-
files =
|
|
257
|
+
files = packages.get_files(project, package_id).keys()
|
|
141
258
|
for file in files:
|
|
142
|
-
ret =
|
|
259
|
+
ret = packages.download_file(project, name, version, file, destination)
|
|
143
260
|
if ret:
|
|
144
261
|
print("Failed to download file " + file)
|
|
145
262
|
break
|
|
@@ -147,37 +264,156 @@ class CLIHandler:
|
|
|
147
264
|
print("No package " + name + " version " + version + " found!")
|
|
148
265
|
return ret
|
|
149
266
|
|
|
150
|
-
def _register_list_parser(self, parser):
|
|
267
|
+
def _register_list_parser(self, parser: argparse.ArgumentParser):
|
|
268
|
+
"""
|
|
269
|
+
Registers the list command related arguments to the parser:
|
|
270
|
+
- action
|
|
271
|
+
|
|
272
|
+
Additionally, registers the common args.
|
|
273
|
+
|
|
274
|
+
Parameters
|
|
275
|
+
----------
|
|
276
|
+
parser:
|
|
277
|
+
The argparser where to register the list arguments.
|
|
278
|
+
"""
|
|
151
279
|
self._register_common_arguments(parser)
|
|
152
280
|
parser.set_defaults(action=self._list_packages)
|
|
153
281
|
|
|
154
282
|
def _list_packages(self, args: argparse.Namespace) -> int:
|
|
283
|
+
"""
|
|
284
|
+
List package versions from GitLab package registry.
|
|
285
|
+
|
|
286
|
+
Parameters
|
|
287
|
+
----------
|
|
288
|
+
args : argparse.Namespace
|
|
289
|
+
The parsed arguments
|
|
290
|
+
|
|
291
|
+
Returns
|
|
292
|
+
-------
|
|
293
|
+
int
|
|
294
|
+
Zero if everything goes well, non-zero otherwise
|
|
295
|
+
"""
|
|
155
296
|
host, project, name, token_user, token = self._args(args)
|
|
156
|
-
|
|
157
|
-
|
|
297
|
+
packages = Packages(host, token_user, token)
|
|
298
|
+
package_list = packages.get_versions(project, name)
|
|
158
299
|
print("Name" + "\t\t" + "Version")
|
|
159
|
-
for package in
|
|
300
|
+
for package in package_list:
|
|
160
301
|
print(package["name"] + "\t" + package["version"])
|
|
161
302
|
|
|
162
|
-
def _register_upload_parser(self, parser):
|
|
303
|
+
def _register_upload_parser(self, parser: argparse.ArgumentParser):
|
|
304
|
+
"""
|
|
305
|
+
Registers the upload command related arguments to the parser:
|
|
306
|
+
- version
|
|
307
|
+
- file
|
|
308
|
+
- action
|
|
309
|
+
|
|
310
|
+
Additionally, registers the common args.
|
|
311
|
+
|
|
312
|
+
Parameters
|
|
313
|
+
----------
|
|
314
|
+
parser:
|
|
315
|
+
The argparser where to register the upload arguments.
|
|
316
|
+
"""
|
|
163
317
|
self._register_common_arguments(parser)
|
|
164
318
|
parser.add_argument("-v", "--version", type=str, help="The package version.")
|
|
165
319
|
parser.add_argument(
|
|
166
320
|
"-f",
|
|
167
321
|
"--file",
|
|
168
322
|
type=str,
|
|
169
|
-
help="The file to be uploaded, for example my_file.txt. Note that
|
|
323
|
+
help="The file to be uploaded, for example my_file.txt. Note that "
|
|
324
|
+
"only relative paths (to the source) are supported and the relative "
|
|
325
|
+
"path is preserved when uploading the file. If left undefined, all files "
|
|
326
|
+
"of the source directory are uploaded. For example --source=temp --file=myfile "
|
|
327
|
+
"will upload myfile to the GitLab generic package root. However using --source=. "
|
|
328
|
+
"(or omittinge source) --file=temp/myfile will upload the file to temp folder "
|
|
329
|
+
"in the GitLab package.",
|
|
330
|
+
)
|
|
331
|
+
parser.add_argument(
|
|
332
|
+
"-s",
|
|
333
|
+
"--source",
|
|
334
|
+
type=str,
|
|
335
|
+
default="",
|
|
336
|
+
help="The source directory of the uploaded file(s). Defaults to current"
|
|
337
|
+
"working directory.",
|
|
170
338
|
)
|
|
171
339
|
parser.set_defaults(action=self._upload)
|
|
172
340
|
|
|
173
|
-
def _upload(self, args) -> int:
|
|
174
|
-
|
|
341
|
+
def _upload(self, args: argparse.Namespace) -> int:
|
|
342
|
+
"""
|
|
343
|
+
Uploads a file to a GitLab package registry
|
|
344
|
+
|
|
345
|
+
Parameters
|
|
346
|
+
----------
|
|
347
|
+
args : argparse.Namespace
|
|
348
|
+
The arguments from command line
|
|
349
|
+
|
|
350
|
+
Returns
|
|
351
|
+
-------
|
|
352
|
+
int
|
|
353
|
+
Zero if everything went fine, non-zero otherwise.
|
|
354
|
+
"""
|
|
355
|
+
ret = 0
|
|
356
|
+
host, project, name, token_user, token = self._args(args)
|
|
357
|
+
version = args.version
|
|
358
|
+
file = args.file
|
|
359
|
+
source = args.source
|
|
360
|
+
if file:
|
|
361
|
+
# Check if the uploaded file exists.
|
|
362
|
+
if not os.path.isfile(os.path.join(source, file)):
|
|
363
|
+
print("File " + file + " does not exist!")
|
|
364
|
+
ret = 1
|
|
365
|
+
if not ret:
|
|
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
|
|
175
411
|
host, project, name, token_user, token = self._args(args)
|
|
176
412
|
version = args.version
|
|
177
413
|
file = args.file
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
ret =
|
|
414
|
+
packages = Packages(host, token_user, token)
|
|
415
|
+
if file:
|
|
416
|
+
ret = packages.delete_file(project, name, version, file)
|
|
181
417
|
else:
|
|
182
|
-
|
|
418
|
+
ret = packages.delete_package(project, name, version)
|
|
183
419
|
return ret
|
gitlab/packages.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
"""GitLab generic packages module"""
|
|
2
|
+
|
|
3
|
+
from glob import glob
|
|
1
4
|
from http.client import HTTPMessage
|
|
2
|
-
import
|
|
5
|
+
from json import loads
|
|
3
6
|
import logging
|
|
4
7
|
import os
|
|
5
8
|
from urllib import request, parse
|
|
@@ -8,71 +11,298 @@ logger = logging.getLogger(__name__)
|
|
|
8
11
|
|
|
9
12
|
|
|
10
13
|
class Packages:
|
|
14
|
+
"""Class to interact with GitLab packages REST API"""
|
|
15
|
+
|
|
11
16
|
def __init__(self, host: str, token_type: str, token: str):
|
|
17
|
+
"""
|
|
18
|
+
Creates a new instance of class.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
host : str
|
|
23
|
+
The GitLab instance hostname, without schema.
|
|
24
|
+
The host will be used for the package API interaction.
|
|
25
|
+
For example gitlab.com.
|
|
26
|
+
token_type : str
|
|
27
|
+
The token type or "user" to authenticate with GitLab REST API.
|
|
28
|
+
For personal, project, and group tokens this is `PRIVATE-TOKEN`.
|
|
29
|
+
For `CI_JOB_TOKEN` this is `JOB-TOKEN`.
|
|
30
|
+
Can be left empty when authentication is not used.
|
|
31
|
+
token : str
|
|
32
|
+
The token (secret) to authenticate with GitLab REST API.
|
|
33
|
+
This can be a personal token, project token, or`CI_JOB_TOKEN`.
|
|
34
|
+
Leave empty when authentication is not used.
|
|
35
|
+
"""
|
|
12
36
|
self.host = host
|
|
13
37
|
self.token_type = token_type
|
|
14
38
|
self.token = token
|
|
15
39
|
|
|
16
|
-
def
|
|
17
|
-
|
|
40
|
+
def _url(self) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Returns the GitLab REST API URL by using the host variable.
|
|
18
43
|
|
|
19
|
-
|
|
20
|
-
|
|
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/"
|
|
21
50
|
|
|
22
|
-
def
|
|
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
|
+
"""
|
|
23
63
|
headers = {}
|
|
24
64
|
if self.token_type and self.token:
|
|
25
65
|
headers = {self.token_type: self.token}
|
|
26
66
|
return headers
|
|
27
67
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return response.status, response.read(), response.headers
|
|
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.
|
|
33
72
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 "/".
|
|
44
111
|
|
|
45
|
-
|
|
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
|
+
"""
|
|
46
138
|
query = ""
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
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
|
|
51
143
|
return query
|
|
52
144
|
|
|
53
|
-
def
|
|
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)
|
|
54
216
|
data = []
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
url = self.project_api_url(project) + path + query
|
|
61
|
-
status, res_data, headers = self._request(url)
|
|
62
|
-
logger.debug("Response status: " + str(status))
|
|
63
|
-
res_data = json.loads(res_data)
|
|
64
|
-
logger.debug("Response data: " + str(res_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)
|
|
65
222
|
data = data + res_data
|
|
66
|
-
|
|
67
|
-
if page:
|
|
68
|
-
more = True
|
|
223
|
+
url = self._parse_header_links(res_headers).get("next")
|
|
69
224
|
return data
|
|
70
225
|
|
|
71
|
-
def
|
|
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:
|
|
282
|
+
"""
|
|
283
|
+
Lists the available versions of the package
|
|
284
|
+
|
|
285
|
+
Parameters
|
|
286
|
+
----------
|
|
287
|
+
project_id : str
|
|
288
|
+
The project ID or path, including namespace.
|
|
289
|
+
Examples: `123` or `namespace/project`.
|
|
290
|
+
package_name : str
|
|
291
|
+
The name of the package that is listed.
|
|
292
|
+
|
|
293
|
+
Returns
|
|
294
|
+
-------
|
|
295
|
+
list
|
|
296
|
+
List of {package: name, version: version} that are available.
|
|
297
|
+
"""
|
|
72
298
|
packages = []
|
|
73
|
-
logger.debug("Listing packages with name "
|
|
74
|
-
data = self.
|
|
75
|
-
|
|
299
|
+
logger.debug("Listing packages with name %s", 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",
|
|
76
306
|
)
|
|
77
307
|
for package in data:
|
|
78
308
|
name = parse.unquote(package["name"])
|
|
@@ -84,93 +314,310 @@ class Packages:
|
|
|
84
314
|
packages.append({"name": name, "version": version})
|
|
85
315
|
return packages
|
|
86
316
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
317
|
+
def get_files(self, project_id: str, package_id: int) -> dict:
|
|
318
|
+
"""
|
|
319
|
+
Lists all files of a specific package ID from GitLab REST API
|
|
320
|
+
|
|
321
|
+
Parameters
|
|
322
|
+
----------
|
|
323
|
+
project_id : str
|
|
324
|
+
The project ID or path, including namespace.
|
|
325
|
+
Examples: `123` or `namespace/project`.
|
|
326
|
+
package_id : int
|
|
327
|
+
The package ID that is listed
|
|
328
|
+
|
|
329
|
+
Return
|
|
330
|
+
------
|
|
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
|
|
335
|
+
"""
|
|
336
|
+
files = {}
|
|
337
|
+
logger.debug("Listing package %d files", package_id)
|
|
338
|
+
data = self.get_all(
|
|
339
|
+
"projects",
|
|
340
|
+
parse.quote_plus(project_id),
|
|
341
|
+
"packages",
|
|
342
|
+
str(package_id),
|
|
343
|
+
"package_files",
|
|
344
|
+
)
|
|
92
345
|
for package in data:
|
|
93
346
|
# Only append the filename once to the list of files
|
|
94
347
|
# as there's no way to download them separately through
|
|
95
348
|
# the API
|
|
96
349
|
filename = parse.unquote(package["file_name"])
|
|
97
|
-
|
|
98
|
-
|
|
350
|
+
file_id = package["id"]
|
|
351
|
+
if not files.get(filename):
|
|
352
|
+
files[filename] = {"id": file_id}
|
|
99
353
|
return files
|
|
100
354
|
|
|
101
|
-
def
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
355
|
+
def get_id(self, project_id: str, package_name: str, package_version: str) -> int:
|
|
356
|
+
"""
|
|
357
|
+
Gets the package ID of a specific package version.
|
|
358
|
+
|
|
359
|
+
Parameters
|
|
360
|
+
----------
|
|
361
|
+
project_id : str
|
|
362
|
+
The project ID or path, including namespace.
|
|
363
|
+
Examples: `123` or `namespace/project`.
|
|
364
|
+
package_name : str
|
|
365
|
+
The name of the package.
|
|
366
|
+
package_version : str
|
|
367
|
+
The version of the package
|
|
368
|
+
|
|
369
|
+
Return
|
|
370
|
+
------
|
|
371
|
+
int
|
|
372
|
+
The ID of the package. -1 if no ID was found.
|
|
373
|
+
"""
|
|
374
|
+
package_id = -1
|
|
375
|
+
logger.debug("Fetching package %s (%s) ID", package_name, 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",
|
|
114
383
|
)
|
|
115
|
-
data = self.gl_project_api(project, path, arg)
|
|
116
384
|
if len(data) == 1:
|
|
117
385
|
package = data.pop()
|
|
118
|
-
|
|
119
|
-
return
|
|
386
|
+
package_id = package["id"]
|
|
387
|
+
return package_id
|
|
120
388
|
|
|
121
389
|
def download_file(
|
|
122
390
|
self,
|
|
123
|
-
|
|
391
|
+
project_id: str,
|
|
124
392
|
package_name: str,
|
|
125
393
|
package_version: str,
|
|
126
394
|
filename: str,
|
|
127
395
|
destination: str = "",
|
|
128
396
|
) -> int:
|
|
397
|
+
"""
|
|
398
|
+
Downloads a file from a GitLab generic package
|
|
399
|
+
|
|
400
|
+
Parameters
|
|
401
|
+
----------
|
|
402
|
+
project_id : str
|
|
403
|
+
The project ID or path, including namespace.
|
|
404
|
+
Examples: `123` or `namespace/project`.
|
|
405
|
+
package_name : str
|
|
406
|
+
The name of the generic package.
|
|
407
|
+
package_version : str
|
|
408
|
+
The version of the generic package
|
|
409
|
+
filename : str
|
|
410
|
+
The file that is downloaded
|
|
411
|
+
destination : str, optional
|
|
412
|
+
The destination folder of the downloaded file. If not set,
|
|
413
|
+
current working directory is used.
|
|
414
|
+
|
|
415
|
+
Return
|
|
416
|
+
------
|
|
417
|
+
int
|
|
418
|
+
Zero if everything went fine, non-zero coke otherwise.
|
|
419
|
+
"""
|
|
129
420
|
ret = 1
|
|
130
|
-
logger.debug("Downloading file "
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
421
|
+
logger.debug("Downloading file %s", 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),
|
|
139
430
|
)
|
|
140
|
-
status, data, _ = self._request(url)
|
|
141
431
|
if status == 200:
|
|
142
|
-
|
|
143
|
-
parent = os.path.dirname(
|
|
432
|
+
fpath = os.path.join(destination, filename)
|
|
433
|
+
parent = os.path.dirname(fpath)
|
|
144
434
|
if parent:
|
|
145
435
|
# Create missing directories if needed
|
|
146
436
|
# In case path has no parent, current
|
|
147
437
|
# workind directory is used
|
|
148
|
-
os.makedirs(os.path.dirname(
|
|
149
|
-
with open(
|
|
438
|
+
os.makedirs(os.path.dirname(fpath), exist_ok=True)
|
|
439
|
+
with open(fpath, "wb") as file:
|
|
150
440
|
file.write(data)
|
|
151
441
|
ret = 0
|
|
152
442
|
return ret
|
|
153
443
|
|
|
154
444
|
def upload_file(
|
|
155
|
-
self,
|
|
445
|
+
self,
|
|
446
|
+
project_id: str,
|
|
447
|
+
package_name: str,
|
|
448
|
+
package_version: str,
|
|
449
|
+
filename: str,
|
|
450
|
+
source: str,
|
|
156
451
|
) -> int:
|
|
452
|
+
"""
|
|
453
|
+
Uploads file(s) to a GitLab generic package.
|
|
454
|
+
|
|
455
|
+
Parameters
|
|
456
|
+
----------
|
|
457
|
+
project_id : str
|
|
458
|
+
The project ID or path, including namespace.
|
|
459
|
+
Examples: `123` or `namespace/project`.
|
|
460
|
+
package_name : str
|
|
461
|
+
The name of the generic package.
|
|
462
|
+
package_version : str
|
|
463
|
+
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
|
+
source : str
|
|
468
|
+
The source folder that is used as root when uploading. If empty,
|
|
469
|
+
current working directory is used.
|
|
470
|
+
|
|
471
|
+
Return
|
|
472
|
+
------
|
|
473
|
+
int
|
|
474
|
+
Zero if everything went fine, non-zero coke otherwise.
|
|
475
|
+
"""
|
|
476
|
+
files = []
|
|
157
477
|
ret = 1
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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))
|
|
487
|
+
for afile in files:
|
|
488
|
+
ret = self._upload_file(
|
|
489
|
+
project_id, package_name, package_version, afile, source
|
|
168
490
|
)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
491
|
+
if ret:
|
|
492
|
+
break
|
|
493
|
+
return ret
|
|
494
|
+
|
|
495
|
+
def _upload_file(
|
|
496
|
+
self,
|
|
497
|
+
project_id: str,
|
|
498
|
+
package_name: str,
|
|
499
|
+
package_version: str,
|
|
500
|
+
filename: str,
|
|
501
|
+
source: str,
|
|
502
|
+
) -> int:
|
|
503
|
+
"""
|
|
504
|
+
Uploads a file to a GitLab generic package.
|
|
505
|
+
|
|
506
|
+
Parameters
|
|
507
|
+
----------
|
|
508
|
+
project_id : str
|
|
509
|
+
The project ID or path, including namespace.
|
|
510
|
+
Examples: `123` or `namespace/project`.
|
|
511
|
+
package_name : str
|
|
512
|
+
The name of the generic package.
|
|
513
|
+
package_version : str
|
|
514
|
+
The version of the generic package
|
|
515
|
+
filename : str
|
|
516
|
+
The relative path of the file that is uploaded.
|
|
517
|
+
source : str
|
|
518
|
+
The source folder that is used as root when uploading.
|
|
519
|
+
|
|
520
|
+
Return
|
|
521
|
+
------
|
|
522
|
+
int
|
|
523
|
+
Zero if everything went fine, non-zero coke otherwise.
|
|
524
|
+
"""
|
|
525
|
+
ret = 1
|
|
526
|
+
fpath = os.path.join(source, filename)
|
|
527
|
+
logger.debug("Uploading file %s from %s", filename, source)
|
|
528
|
+
with open(fpath, "rb") as data:
|
|
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),
|
|
173
538
|
)
|
|
174
|
-
|
|
175
|
-
|
|
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)
|
|
567
|
+
)
|
|
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
|
+
)
|
|
176
623
|
return ret
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: glpkg
|
|
3
|
-
Version: 1.
|
|
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
|
```
|
|
@@ -105,10 +105,49 @@ Where:
|
|
|
105
105
|
- `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`)
|
|
106
106
|
- `mypackagename` is the name of the generic package
|
|
107
107
|
- `1.0` is the version of the generic package to which the file is uploaded
|
|
108
|
-
- `my-file.txt` is the file that is uploaded to the generic package.
|
|
108
|
+
- `my-file.txt` is the file that is uploaded to the generic package. Only relative paths are supported, and the relative path (e.g. `folder/file.txt`) is preserved when uploading the file to the package.
|
|
109
109
|
|
|
110
110
|
> A GitLab generic package may have multiple files with the same file name. However, it likely is not a great idea, as they cannot be downloaded separately from the GitLab API.
|
|
111
111
|
|
|
112
|
+
To upload multiple files, or to upload a single file from a different directory, use `--source` argument. If no `--file` argument is set, all of the files in the source directory are uploaded, recursively. As an example, to upload all files from a `upload` folder to the package:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
glpkg upload --project 12345 --name mypackagename --version 1.0 --source upload
|
|
116
|
+
```
|
|
117
|
+
|
|
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
|
+
|
|
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
|
+
|
|
112
151
|
### Use in GitLab pipelines
|
|
113
152
|
|
|
114
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`.
|
|
@@ -120,11 +159,3 @@ glpkg upload --ci --name mypackagename --version 1.0 --file my-file.txt
|
|
|
120
159
|
```
|
|
121
160
|
|
|
122
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.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
## Limitations
|
|
126
|
-
|
|
127
|
-
The tool is not perfect (yet) and has limitations. The following limitations are known, but more can exist:
|
|
128
|
-
|
|
129
|
-
- Uploading files must be done one-by-one.
|
|
130
|
-
- 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,,
|
glpkg-1.2.0.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
gitlab/__init__.py,sha256=U83zG_KkowpY9GMPLmmySx5fyNx52nojUJkfIpzMo5o,60
|
|
2
|
-
gitlab/__main__.py,sha256=88VNY5Qrmn8g0rNcnjKNdN746--0chHsKBMH3PD3Nao,177
|
|
3
|
-
gitlab/cli_handler.py,sha256=RKEEcbxFCFkT8sbYDlMXuQPM4wvCb0okBiFfUXXlJtg,7314
|
|
4
|
-
gitlab/packages.py,sha256=8w5bc5I03dEqkMcLydqtgQ9UN2SJWw7MlkjnJXp6fmA,6099
|
|
5
|
-
glpkg-1.2.0.dist-info/licenses/LICENSE.md,sha256=josGXvZq628dNS0Iru58-DPE7dRpDXzjJxKKT35103g,1065
|
|
6
|
-
glpkg-1.2.0.dist-info/METADATA,sha256=cVhMx6Gt_Nx1RkXNeetlu8f7__dkO5y1c2jOncFlZT8,5841
|
|
7
|
-
glpkg-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
-
glpkg-1.2.0.dist-info/entry_points.txt,sha256=xHPZwx2oShYDZ3AyH7WSIvuhFMssy7QLlQk-JAbje_w,46
|
|
9
|
-
glpkg-1.2.0.dist-info/top_level.txt,sha256=MvIaP8p_Oaf4gO_hXmHkX-5y2deHLp1pe6tJR3ukQ6o,7
|
|
10
|
-
glpkg-1.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|