glpkg 1.2.0__py3-none-any.whl → 1.3.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
@@ -1,3 +1,5 @@
1
+ """GitLab packages initialization module"""
2
+
1
3
  from gitlab.packages import Packages
2
4
 
3
- __version__ = "1.2.0"
5
+ __version__ = "1.3.0"
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,12 +26,14 @@ 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 package registry.",
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 to the current directory.",
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(
@@ -30,11 +42,34 @@ class CLIHandler:
30
42
  self._register_upload_parser(upload_parser)
31
43
  self.args = parser.parse_args()
32
44
 
33
- def _print_version(self, args) -> int:
45
+ def _print_version(self, _args: argparse.Namespace) -> int:
46
+ """
47
+ A handler for printing the version of the tool to the console.
48
+
49
+ Parameters
50
+ ----------
51
+ _args : argparse.Namespace
52
+ Unused.
53
+
54
+ Return
55
+ ------
56
+ int
57
+ Zero when printing to console succeeded.
58
+ """
34
59
  print(__version__)
35
60
  return 0
36
61
 
37
62
  def do_it(self) -> int:
63
+ """
64
+ Executes the requested command.
65
+
66
+ In case of error, prints to stderr.
67
+
68
+ Return
69
+ ------
70
+ int
71
+ Zero when everything went fine, non-zero otherwise.
72
+ """
38
73
  ret = 1
39
74
  try:
40
75
  ret = self.args.action(self.args)
@@ -50,20 +85,38 @@ class CLIHandler:
50
85
  print("Check your arguments and credentials.", file=sys.stderr)
51
86
  return ret
52
87
 
53
- def _register_common_arguments(self, parser) -> None:
88
+ def _register_common_arguments(self, parser: argparse.ArgumentParser) -> None:
89
+ """
90
+ Registers common arguments to the parser:
91
+ - host
92
+ - ci
93
+ - project
94
+ - name
95
+ - token
96
+ - netrc
97
+
98
+ Parameters
99
+ ----------
100
+ parser:
101
+ The argparser where to register the common arguments.
102
+ """
54
103
  group = parser.add_mutually_exclusive_group()
55
104
  group.add_argument(
56
105
  "-H",
57
106
  "--host",
58
107
  default="gitlab.com",
59
108
  type=str,
60
- help="The host address of GitLab instance without scheme, for example gitlab.com. Note that only https scheme is supported.",
109
+ help="The host address of GitLab instance without scheme, "
110
+ "for example gitlab.com. Note that only https scheme is supported.",
61
111
  )
62
112
  group.add_argument(
63
113
  "-c",
64
114
  "--ci",
65
115
  action="store_true",
66
- help="Use this in GitLab jobs. In this case CI_SERVER_HOST, CI_PROJECT_ID, and CI_JOB_TOKEN variables from the environment are used. --project and --token can be used to override project ID and the CI_JOB_TOKEN to a personal or project access token.",
116
+ help="Use this in GitLab jobs. In this case CI_SERVER_HOST, CI_PROJECT_ID, "
117
+ "and CI_JOB_TOKEN variables from the environment are used. --project and --token "
118
+ "can be used to override project ID and the CI_JOB_TOKEN to a personal or project "
119
+ "access token.",
67
120
  )
68
121
  parser.add_argument(
69
122
  "-p",
@@ -77,33 +130,77 @@ class CLIHandler:
77
130
  "-t",
78
131
  "--token",
79
132
  type=str,
80
- help="Private or project access token that is used to authenticate with the package registry. Leave empty if the registry is public. The token must have 'read API' or 'API' scope.",
133
+ help="Private or project access token that is used to authenticate with "
134
+ "the package registry. Leave empty if the registry is public. The token "
135
+ "must have 'read API' or 'API' scope.",
81
136
  )
82
137
  group2.add_argument(
83
138
  "--netrc",
84
139
  action="store_true",
85
- help="Set to use a token from .netrc file (~/.netrc) for the host. The .netrc username is ignored due to API restrictions. PRIVATE-TOKEN is used instead. Note that .netrc file access rights must be correct.",
140
+ help="Set to use a token from .netrc file (~/.netrc) for the host. The "
141
+ ".netrc username is ignored due to API restrictions. PRIVATE-TOKEN is used "
142
+ "instead. Note that .netrc file access rights must be correct.",
86
143
  )
87
144
 
88
- def _register_download_parser(self, parser):
145
+ def _register_download_parser(self, parser: argparse.ArgumentParser):
146
+ """
147
+ Registers the download command related arguments to the parser:
148
+ - version
149
+ - file
150
+ - destination
151
+ - action
152
+
153
+ Additionally, registers the common args.
154
+
155
+ Parameters
156
+ ----------
157
+ parser:
158
+ The argparser where to register the download arguments.
159
+ """
89
160
  self._register_common_arguments(parser)
90
161
  parser.add_argument("-v", "--version", type=str, help="The package version.")
91
162
  parser.add_argument(
92
163
  "-f",
93
164
  "--file",
94
165
  type=str,
95
- help="The file to download from the package. If not defined, all files are downloaded.",
166
+ help="The file to download from the package. If not defined, all "
167
+ "files are downloaded.",
96
168
  )
97
169
  parser.add_argument(
98
170
  "-d",
99
171
  "--destination",
100
172
  default="",
101
173
  type=str,
102
- help="The path where the file(s) are downloaded. If not defined, the current working directory is used.",
174
+ help="The path where the file(s) are downloaded. If not defined, "
175
+ "the current working directory is used.",
103
176
  )
104
177
  parser.set_defaults(action=self._download_handler)
105
178
 
106
- def _args(self, args):
179
+ def _args(self, args) -> tuple[str, str, str, str, str]:
180
+ """
181
+ Returns the connection parameters according to the args
182
+
183
+ Parameters
184
+ ----------
185
+ args:
186
+ The args that are used to determined the connection parameters
187
+
188
+ Returns
189
+ -------
190
+ host : str
191
+ The GitLab host name
192
+ project : str
193
+ The Project ID or name to use
194
+ name : str
195
+ The package name
196
+ token_user : str
197
+ The token user according to the args. If ci is used, returns `JOB-TOKEN`, else
198
+ `PRIVATE-TOKEN`.
199
+ token : str
200
+ The token according to the args. If token is set, returns it. If netrc is set,
201
+ reads the token from the .netrc file. If ci is set, reads the environment
202
+ variable CI_JOB_TOKEN. Otherwise returns None.
203
+ """
107
204
  if args.ci:
108
205
  host = os.environ["CI_SERVER_HOST"]
109
206
  project = os.environ["CI_PROJECT_ID"]
@@ -120,12 +217,25 @@ class CLIHandler:
120
217
  token = args.token
121
218
  token_user = "PRIVATE-TOKEN"
122
219
  if args.netrc:
123
- _, _, token = netrc.netrc().authenticators(host)
220
+ _, _, token = netrc().authenticators(host)
124
221
  token_user = "PRIVATE-TOKEN"
125
222
  name = args.name
126
223
  return host, project, name, token_user, token
127
224
 
128
- def _download_handler(self, args) -> int:
225
+ def _download_handler(self, args: argparse.Namespace) -> int:
226
+ """
227
+ Downloads package file(s) from GitLab package registry.
228
+
229
+ Parameters
230
+ ----------
231
+ args : argparse.Namespace
232
+ The parsed arguments
233
+
234
+ Returns
235
+ -------
236
+ int
237
+ Zero if everything goes well, non-zero otherwise
238
+ """
129
239
  ret = 1
130
240
  host, project, name, token_user, token = self._args(args)
131
241
  version = args.version
@@ -147,11 +257,35 @@ class CLIHandler:
147
257
  print("No package " + name + " version " + version + " found!")
148
258
  return ret
149
259
 
150
- def _register_list_parser(self, parser):
260
+ def _register_list_parser(self, parser: argparse.ArgumentParser):
261
+ """
262
+ Registers the list command related arguments to the parser:
263
+ - action
264
+
265
+ Additionally, registers the common args.
266
+
267
+ Parameters
268
+ ----------
269
+ parser:
270
+ The argparser where to register the list arguments.
271
+ """
151
272
  self._register_common_arguments(parser)
152
273
  parser.set_defaults(action=self._list_packages)
153
274
 
154
275
  def _list_packages(self, args: argparse.Namespace) -> int:
276
+ """
277
+ List package versions from GitLab package registry.
278
+
279
+ Parameters
280
+ ----------
281
+ args : argparse.Namespace
282
+ The parsed arguments
283
+
284
+ Returns
285
+ -------
286
+ int
287
+ Zero if everything goes well, non-zero otherwise
288
+ """
155
289
  host, project, name, token_user, token = self._args(args)
156
290
  gitlab = Packages(host, token_user, token)
157
291
  packages = gitlab.list_packages(project, name)
@@ -159,25 +293,69 @@ class CLIHandler:
159
293
  for package in packages:
160
294
  print(package["name"] + "\t" + package["version"])
161
295
 
162
- def _register_upload_parser(self, parser):
296
+ def _register_upload_parser(self, parser: argparse.ArgumentParser):
297
+ """
298
+ Registers the upload command related arguments to the parser:
299
+ - version
300
+ - file
301
+ - action
302
+
303
+ Additionally, registers the common args.
304
+
305
+ Parameters
306
+ ----------
307
+ parser:
308
+ The argparser where to register the upload arguments.
309
+ """
163
310
  self._register_common_arguments(parser)
164
311
  parser.add_argument("-v", "--version", type=str, help="The package version.")
165
312
  parser.add_argument(
166
313
  "-f",
167
314
  "--file",
168
315
  type=str,
169
- help="The file to be uploaded, for example my_file.txt. Note that only relative paths are supported and the relative path is preserved when uploading the file.",
316
+ help="The file to be uploaded, for example my_file.txt. Note that "
317
+ "only relative paths (to the source) are supported and the relative "
318
+ "path is preserved when uploading the file. If left undefined, all files "
319
+ "of the source directory are uploaded. For example --source=temp --file=myfile "
320
+ "will upload myfile to the GitLab generic package root. However using --source=. "
321
+ "(or omittinge source) --file=temp/myfile will upload the file to temp folder "
322
+ "in the GitLab package.",
323
+ )
324
+ parser.add_argument(
325
+ "-s",
326
+ "--source",
327
+ type=str,
328
+ default="",
329
+ help="The source directory of the uploaded file(s). Defaults to current"
330
+ "working directory.",
170
331
  )
171
332
  parser.set_defaults(action=self._upload)
172
333
 
173
- def _upload(self, args) -> int:
174
- ret = 1
334
+ def _upload(self, args: argparse.Namespace) -> int:
335
+ """
336
+ Uploads a file to a GitLab package registry
337
+
338
+ Parameters
339
+ ----------
340
+ args : argparse.Namespace
341
+ The arguments from command line
342
+
343
+ Returns
344
+ -------
345
+ int
346
+ Zero if everything went fine, non-zero otherwise.
347
+ """
348
+ ret = 0
175
349
  host, project, name, token_user, token = self._args(args)
176
350
  version = args.version
177
351
  file = args.file
178
- if os.path.isfile(file):
352
+ source = args.source
353
+ if file:
354
+ # Check if the uploaded file exists.
355
+ if not os.path.isfile(os.path.join(source, file)):
356
+ print("File " + file + " does not exist!")
357
+ ret = 1
358
+ if not ret:
179
359
  gitlab = Packages(host, token_user, token)
180
- ret = gitlab.upload_file(project, name, version, file)
181
- else:
182
- print("File " + file + " does not exist!")
360
+ ret = gitlab.upload_file(project, name, version, file, source)
183
361
  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 json
5
+ from json import loads
3
6
  import logging
4
7
  import os
5
8
  from urllib import request, parse
@@ -8,60 +11,189 @@ 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 Packages 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
40
  def api_url(self) -> str:
17
- return "https://{}/api/v4/".format(parse.quote(self.host))
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/"
18
50
 
19
51
  def project_api_url(self, project: str) -> str:
20
- return self.api_url() + "projects/{}/".format(parse.quote_plus(project))
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)}/"
21
68
 
22
69
  def get_headers(self) -> dict:
70
+ """
71
+ Creates headers for a GitLab REST API call.
72
+
73
+ The headers contain token for authentication according to the
74
+ instance variables.
75
+
76
+ Returns
77
+ -------
78
+ dict
79
+ Headers for a REST API request, that contain the authentication token.
80
+ """
23
81
  headers = {}
24
82
  if self.token_type and self.token:
25
83
  headers = {self.token_type: self.token}
26
84
  return headers
27
85
 
28
86
  def _request(self, url: str) -> tuple[int, bytes, HTTPMessage]:
29
- logger.debug("Requesting " + url)
87
+ """
88
+ Makes a HTTP request to the given URL, and returns
89
+ the response status, body, and headers.
90
+
91
+
92
+ Parameters
93
+ ----------
94
+ url : str
95
+ The URL of the HTTP request to make.
96
+
97
+ Returns
98
+ -------
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
105
+ """
106
+ logger.debug("Requesting %s", url)
30
107
  req = request.Request(url, headers=self.get_headers())
31
108
  with request.urlopen(req) as response:
32
109
  return response.status, response.read(), response.headers
33
110
 
34
111
  def _get_next_page(self, headers: HTTPMessage) -> int:
112
+ """
113
+ Returns the next page from headers for pagination.
114
+
115
+ Uses the field x-next-page from the headers.
116
+
117
+ Parameters
118
+ ----------
119
+ headers : HTTPMessage
120
+ The header from which to get the next page number for
121
+ pagination
122
+
123
+ Returns
124
+ -------
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
+ """
35
130
  ret = 0
36
131
  if headers:
37
132
  next_page = headers.get("x-next-page")
38
133
  if next_page:
39
134
  ret = int(next_page)
40
- logger.debug("Response incomplete, next page is " + next_page)
135
+ logger.debug("Response incomplete, next page is %s", next_page)
41
136
  else:
42
137
  logger.debug("Response complete")
43
138
  return ret
44
139
 
45
140
  def _build_query(self, arg: str, page: int) -> str:
141
+ """
142
+ Builds a query for a GitLab REST API request
143
+
144
+ Parameters
145
+ ----------
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.
151
+
152
+ Returns
153
+ -------
154
+ str
155
+ A query string for a REST API request. Append this to
156
+ the request URL. Example `?arg=this&page=3`.
157
+ """
46
158
  query = ""
47
159
  if arg or page:
48
160
  if page:
49
161
  page = "page=" + str(page)
50
- query = "?{}".format("&".join(filter(None, (arg, page))))
162
+ query = f"?{'&'.join(filter(None, (arg, page)))}"
51
163
  return query
52
164
 
53
- def gl_project_api(self, project: str, path: str, arg: str = None) -> list:
165
+ def gl_project_api(self, project: str, apath: str, arg: str = None) -> list:
166
+ """
167
+ Returns data from the project REST API for the path. In case
168
+ of multiple pages, all data will be returned.
169
+
170
+ Parameters
171
+ ----------
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
178
+ Additional arguments for the query of the URL, for example to filter
179
+ results: package_name=mypackage
180
+
181
+ Returns
182
+ -------
183
+ list
184
+ Data from GitLab REST API endpoint with the arguments.
185
+ """
54
186
  data = []
55
187
  more = True
56
188
  page = None
57
189
  while more:
58
190
  more = False
59
191
  query = self._build_query(arg, page)
60
- url = self.project_api_url(project) + path + query
192
+ url = self.project_api_url(project) + apath + query
61
193
  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))
194
+ logger.debug("Response status: %d", status)
195
+ res_data = loads(res_data)
196
+ logger.debug("Response data: %s", res_data)
65
197
  data = data + res_data
66
198
  page = self._get_next_page(headers)
67
199
  if page:
@@ -69,8 +201,24 @@ class Packages:
69
201
  return data
70
202
 
71
203
  def list_packages(self, project: str, package_name: str) -> list:
204
+ """
205
+ Lists the available versions of the package
206
+
207
+ Parameters
208
+ ----------
209
+ project : str
210
+ The project ID or the path of the project, including namespace.
211
+ Examples: `123` or `namespace/project`.
212
+ package_name : str
213
+ The name of the package that is listed.
214
+
215
+ Returns
216
+ -------
217
+ list
218
+ List of {package: name, version: version} that are available.
219
+ """
72
220
  packages = []
73
- logger.debug("Listing packages with name " + package_name)
221
+ logger.debug("Listing packages with name %s", package_name)
74
222
  data = self.gl_project_api(
75
223
  project, "packages", "package_name=" + parse.quote_plus(package_name)
76
224
  )
@@ -85,10 +233,26 @@ class Packages:
85
233
  return packages
86
234
 
87
235
  def list_files(self, project: str, package_id: int) -> list:
236
+ """
237
+ Lists all files of a specific package ID from GitLab REST API
238
+
239
+ Parameters
240
+ ----------
241
+ project : str
242
+ The project ID or the path of the project, including namespace.
243
+ Examples: `123` or `namespace/project`.
244
+ package_id : int
245
+ The package ID that is listed
246
+
247
+ Return
248
+ ------
249
+ list
250
+ List of file (names) that are in the package.
251
+ """
88
252
  files = []
89
- logger.debug("Listing package " + str(package_id) + " files")
90
- path = "packages/" + parse.quote_plus(str(package_id)) + "/package_files"
91
- data = self.gl_project_api(project, path)
253
+ 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)
92
256
  for package in data:
93
257
  # Only append the filename once to the list of files
94
258
  # as there's no way to download them separately through
@@ -101,22 +265,38 @@ class Packages:
101
265
  def get_package_id(
102
266
  self, project: str, package_name: str, package_version: str
103
267
  ) -> int:
104
- id = 0
105
- logger.debug(
106
- "Fetching package " + package_name + " (" + package_version + ") ID"
107
- )
108
- path = "packages"
268
+ """
269
+ Gets the package ID of a specific package version.
270
+
271
+ Parameters
272
+ ----------
273
+ project : str
274
+ The project ID or the path of the project, including namespace.
275
+ Examples: `123` or `namespace/project`.
276
+ package_name : str
277
+ The name of the package.
278
+ package_version : str
279
+ The version of the package
280
+
281
+ Return
282
+ ------
283
+ int
284
+ The ID of the package. Zero if no ID was found.
285
+ """
286
+ package_id = 0
287
+ logger.debug("Fetching package %s (%s) ID", package_name, package_version)
288
+ apath = "packages"
109
289
  arg = (
110
290
  "package_name="
111
291
  + parse.quote_plus(package_name)
112
292
  + "&package_version="
113
293
  + parse.quote_plus(package_version)
114
294
  )
115
- data = self.gl_project_api(project, path, arg)
295
+ data = self.gl_project_api(project, apath, arg)
116
296
  if len(data) == 1:
117
297
  package = data.pop()
118
- id = package["id"]
119
- return id
298
+ package_id = package["id"]
299
+ return package_id
120
300
 
121
301
  def download_file(
122
302
  self,
@@ -126,8 +306,31 @@ class Packages:
126
306
  filename: str,
127
307
  destination: str = "",
128
308
  ) -> int:
309
+ """
310
+ Downloads a file from a GitLab generic package
311
+
312
+ Parameters
313
+ ----------
314
+ project : str
315
+ The project ID or the path of the project, including namespace.
316
+ Examples: `123` or `namespace/project`.
317
+ package_name : str
318
+ The name of the generic package.
319
+ package_version : str
320
+ The version of the generic package
321
+ filename : str
322
+ The file that is downloaded
323
+ destination : str, optional
324
+ The destination folder of the downloaded file. If not set,
325
+ current working directory is used.
326
+
327
+ Return
328
+ ------
329
+ int
330
+ Zero if everything went fine, non-zero coke otherwise.
331
+ """
129
332
  ret = 1
130
- logger.debug("Downloading file " + filename)
333
+ logger.debug("Downloading file %s", filename)
131
334
  url = (
132
335
  self.project_api_url(project)
133
336
  + "packages/generic/"
@@ -139,24 +342,103 @@ class Packages:
139
342
  )
140
343
  status, data, _ = self._request(url)
141
344
  if status == 200:
142
- path = os.path.join(destination, filename)
143
- parent = os.path.dirname(path)
345
+ fpath = os.path.join(destination, filename)
346
+ parent = os.path.dirname(fpath)
144
347
  if parent:
145
348
  # Create missing directories if needed
146
349
  # In case path has no parent, current
147
350
  # workind directory is used
148
- os.makedirs(os.path.dirname(path), exist_ok=True)
149
- with open(path, "wb") as file:
351
+ os.makedirs(os.path.dirname(fpath), exist_ok=True)
352
+ with open(fpath, "wb") as file:
150
353
  file.write(data)
151
354
  ret = 0
152
355
  return ret
153
356
 
154
357
  def upload_file(
155
- self, project: str, package_name: str, package_version: str, file: str
358
+ self,
359
+ project: str,
360
+ package_name: str,
361
+ package_version: str,
362
+ pfile: str,
363
+ source: str,
156
364
  ) -> int:
365
+ """
366
+ Uploads file(s) to a GitLab generic package.
367
+
368
+ Parameters
369
+ ----------
370
+ project : str
371
+ The project ID or the path of the project, including namespace.
372
+ Examples: `123` or `namespace/project`.
373
+ package_name : str
374
+ The name of the generic package.
375
+ package_version : str
376
+ The version of the generic package
377
+ file : str
378
+ The relative path of the file that is uploaded. If left empty,
379
+ all files from the source folder, and it's subfolders, are uploaded.
380
+ source : str
381
+ The source folder that is used as root when uploading. If empty,
382
+ current working directory is used.
383
+
384
+ Return
385
+ ------
386
+ int
387
+ Zero if everything went fine, non-zero coke otherwise.
388
+ """
389
+ files = []
157
390
  ret = 1
158
- logger.debug("Uploading file " + file)
159
- with open(str(file), "rb") as data:
391
+ if pfile:
392
+ files.append(pfile)
393
+ else:
394
+ filelist = glob(os.path.join(source, "**"), recursive=True)
395
+ for item in filelist:
396
+ # Only add files, not folders
397
+ if os.path.isfile(os.path.join(item)):
398
+ # Remove the source folder from the path of the files
399
+ files.append(os.path.relpath(item, source))
400
+ for ufile in files:
401
+ ret = self._upload_file(
402
+ project, package_name, package_version, ufile, source
403
+ )
404
+ if ret:
405
+ break
406
+ return ret
407
+
408
+ def _upload_file(
409
+ self,
410
+ project: str,
411
+ package_name: str,
412
+ package_version: str,
413
+ pfile: str,
414
+ source: str,
415
+ ) -> int:
416
+ """
417
+ Uploads a file to a GitLab generic package.
418
+
419
+ Parameters
420
+ ----------
421
+ project : str
422
+ The project ID or the path of the project, including namespace.
423
+ Examples: `123` or `namespace/project`.
424
+ package_name : str
425
+ The name of the generic package.
426
+ package_version : str
427
+ The version of the generic package
428
+ file : str
429
+ The relative path of the file that is uploaded.
430
+ source : str
431
+ The source folder that is used as root when uploading.
432
+
433
+ Return
434
+ ------
435
+ int
436
+ Zero if everything went fine, non-zero coke otherwise.
437
+ """
438
+ ret = 1
439
+ fpath = os.path.join(source, pfile)
440
+ logger.debug("Uploading file %s from %s", pfile, source)
441
+ with open(fpath, "rb") as data:
160
442
  url = (
161
443
  self.project_api_url(project)
162
444
  + "packages/generic/"
@@ -164,13 +446,12 @@ class Packages:
164
446
  + "/"
165
447
  + parse.quote_plus(package_version)
166
448
  + "/"
167
- + parse.quote(str(file))
449
+ + parse.quote(pfile)
168
450
  )
169
- res = request.urlopen(
170
- request.Request(
171
- url, method="PUT", data=data, headers=self.get_headers()
172
- )
451
+ req = request.Request(
452
+ url, method="PUT", data=data, headers=self.get_headers()
173
453
  )
174
- if res.status == 201: # 201 is created
175
- ret = 0
454
+ with request.urlopen(req) as res:
455
+ if res.status == 201: # 201 is created
456
+ ret = 0
176
457
  return ret
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: glpkg
3
- Version: 1.2.0
3
+ Version: 1.3.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
@@ -105,10 +105,18 @@ 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. Currently, only relative paths are supported, and the relative path (e.g. `folder/file.txt`) is preserved when uploading the file to the registry.
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
+
112
120
  ### Use in GitLab pipelines
113
121
 
114
122
  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`.
@@ -126,5 +134,4 @@ To use the `CI_JOB_TOKEN` with package registry of another projects, add `--proj
126
134
 
127
135
  The tool is not perfect (yet) and has limitations. The following limitations are known, but more can exist:
128
136
 
129
- - Uploading files must be done one-by-one.
130
137
  - Only project registries are supported for now.
@@ -0,0 +1,10 @@
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,,
@@ -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