glpkg 1.4.1__tar.gz → 1.5.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: glpkg
3
- Version: 1.4.1
3
+ Version: 1.5.0
4
4
  Summary: Tool to make GitLab generic package registry operations easy.
5
5
  Author-email: bugproduction <bugproduction@outlook.com>
6
6
  License-Expression: MIT
@@ -25,7 +25,7 @@ Install the tool from with pip:
25
25
  pip install glpkg
26
26
  ```
27
27
 
28
- To check the installation and version, run:
28
+ To check the installation and version, run:
29
29
 
30
30
  ```bash
31
31
  glpkg --version
@@ -35,6 +35,14 @@ If you see a version in the terminal, you're good to go!
35
35
 
36
36
  ## Usage
37
37
 
38
+ When in doubt
39
+
40
+ ```bash
41
+ glpkg --help
42
+ ```
43
+
44
+ might help
45
+
38
46
  By default, the used GitLab host is gitlab.com. If you use a self-hosted GitLab, use argument `--host my-gitlab.net` with the commands.
39
47
 
40
48
  > Only https scheme is supported.
@@ -93,6 +101,29 @@ glpkg download --project 12345 --name mypackagename --version 1.5 --file the_onl
93
101
 
94
102
  > If a package has multiple files with the same filename, the tool can only download the newest file. This is a restriction of GitLab API.
95
103
 
104
+ #### Downloading a list of packages
105
+
106
+ To download multiple packages or versions from one or more projects, a package file can be used together with `--from-file` argument with `download`:
107
+
108
+ ```bash
109
+ glpkg download --from-file my-packages.txt --destination my-downloads
110
+ ```
111
+
112
+ The `my-packages.txt` lists all wanted packages with their versions and projects in format `<package-name>==<package-version>@<project-id>`.For example `my-packages.txt` could look like this:
113
+
114
+ ```
115
+ mypackage==2.3.6-beta@namespace/project
116
+ yourpackage==1.2.3@12345
117
+ ```
118
+
119
+ The `<project-id>` can be either the path to the project in string format or the project ID in numeric.
120
+
121
+ When `--from-file` argument is used, `--file`, `--project`, `--version`, and `--name` arguments are unused.
122
+
123
+ > The `--destination` argument can be used to download the files from the packages to a different folder. In case packages contains files with same names, some files will be overwritten.
124
+
125
+ > Whatever credentials (`--token`, `--ci`, or `--netrc`) you run the download command with will be used for all package downloads in the list. Make sure that the credential has access to all projects.
126
+
96
127
  ### Upload a file to a generic package
97
128
 
98
129
  To upload a file to a version of a generic package, run
@@ -150,7 +181,7 @@ The token that is used to delete packages must have at least Maintainer role in
150
181
 
151
182
  ### Use in GitLab pipelines
152
183
 
153
- If you use the tool in a GitLab pipeline, setting argument `--ci` uses [GitLab predefined variables](https://docs.gitlab.com/ci/variables/predefined_variables/) to configure the tool. In this case `CI_SERVER_HOST`, `CI_PROJECT_ID`, and `CI_JOB_TOKEN` environment variables are used. The `--project`, and `--token` arguments can still be used to override the project ID or to use a personal or project access token instead of `CI_JOB_TOKEN`.
184
+ If you use the tool in a GitLab pipeline, setting argument `--ci` uses [GitLab predefined variables](https://docs.gitlab.com/ci/variables/predefined_variables/) to configure the tool. In this case `CI_SERVER_HOST`, `CI_PROJECT_ID`, and `CI_JOB_TOKEN` environment variables are used. The `--host`, `--project`, and `--token` arguments can still be used to override the host, project ID, or to use a personal or project access token instead of `CI_JOB_TOKEN`.
154
185
 
155
186
  In other words, you don't need to give the `--host`, `--project`, or `--token` arguments if you are interacting with the package registry of the project where the pipeline is running. Example: uploading `my-file.txt` to generic package `mypackagename` version `1.0` in the project package registry in CI:
156
187
 
@@ -12,7 +12,7 @@ Install the tool from with pip:
12
12
  pip install glpkg
13
13
  ```
14
14
 
15
- To check the installation and version, run:
15
+ To check the installation and version, run:
16
16
 
17
17
  ```bash
18
18
  glpkg --version
@@ -22,6 +22,14 @@ If you see a version in the terminal, you're good to go!
22
22
 
23
23
  ## Usage
24
24
 
25
+ When in doubt
26
+
27
+ ```bash
28
+ glpkg --help
29
+ ```
30
+
31
+ might help
32
+
25
33
  By default, the used GitLab host is gitlab.com. If you use a self-hosted GitLab, use argument `--host my-gitlab.net` with the commands.
26
34
 
27
35
  > Only https scheme is supported.
@@ -80,6 +88,29 @@ glpkg download --project 12345 --name mypackagename --version 1.5 --file the_onl
80
88
 
81
89
  > If a package has multiple files with the same filename, the tool can only download the newest file. This is a restriction of GitLab API.
82
90
 
91
+ #### Downloading a list of packages
92
+
93
+ To download multiple packages or versions from one or more projects, a package file can be used together with `--from-file` argument with `download`:
94
+
95
+ ```bash
96
+ glpkg download --from-file my-packages.txt --destination my-downloads
97
+ ```
98
+
99
+ The `my-packages.txt` lists all wanted packages with their versions and projects in format `<package-name>==<package-version>@<project-id>`.For example `my-packages.txt` could look like this:
100
+
101
+ ```
102
+ mypackage==2.3.6-beta@namespace/project
103
+ yourpackage==1.2.3@12345
104
+ ```
105
+
106
+ The `<project-id>` can be either the path to the project in string format or the project ID in numeric.
107
+
108
+ When `--from-file` argument is used, `--file`, `--project`, `--version`, and `--name` arguments are unused.
109
+
110
+ > The `--destination` argument can be used to download the files from the packages to a different folder. In case packages contains files with same names, some files will be overwritten.
111
+
112
+ > Whatever credentials (`--token`, `--ci`, or `--netrc`) you run the download command with will be used for all package downloads in the list. Make sure that the credential has access to all projects.
113
+
83
114
  ### Upload a file to a generic package
84
115
 
85
116
  To upload a file to a version of a generic package, run
@@ -137,7 +168,7 @@ The token that is used to delete packages must have at least Maintainer role in
137
168
 
138
169
  ### Use in GitLab pipelines
139
170
 
140
- 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`.
171
+ If you use the tool in a GitLab pipeline, setting argument `--ci` uses [GitLab predefined variables](https://docs.gitlab.com/ci/variables/predefined_variables/) to configure the tool. In this case `CI_SERVER_HOST`, `CI_PROJECT_ID`, and `CI_JOB_TOKEN` environment variables are used. The `--host`, `--project`, and `--token` arguments can still be used to override the host, project ID, or to use a personal or project access token instead of `CI_JOB_TOKEN`.
141
172
 
142
173
  In other words, you don't need to give the `--host`, `--project`, or `--token` arguments if you are interacting with the package registry of the project where the pipeline is running. Example: uploading `my-file.txt` to generic package `mypackagename` version `1.0` in the project package registry in CI:
143
174
 
@@ -2,4 +2,4 @@
2
2
 
3
3
  from gitlab.packages import Packages
4
4
 
5
- __version__ = "1.4.1"
5
+ __version__ = "1.5.0"
@@ -3,6 +3,7 @@
3
3
  import argparse
4
4
  from netrc import netrc
5
5
  import os
6
+ import re
6
7
  import sys
7
8
  import urllib
8
9
  from gitlab import Packages, __version__
@@ -21,9 +22,8 @@ class CLIHandler:
21
22
  parser = argparse.ArgumentParser(
22
23
  description="Toolbox for GitLab generic packages"
23
24
  )
24
- parser.add_argument("-v", "--version", action="store_true")
25
- parser.set_defaults(action=self._print_version)
26
- subparsers = parser.add_subparsers()
25
+ parser.add_argument("-v", "--version", action="version", version=__version__)
26
+ subparsers = parser.add_subparsers(required=True)
27
27
  list_parser = subparsers.add_parser(
28
28
  name="list",
29
29
  description="Lists the available version of a package from the "
@@ -49,23 +49,6 @@ class CLIHandler:
49
49
  self._register_delete_parser(delete_parser)
50
50
  self.args = parser.parse_args()
51
51
 
52
- def _print_version(self, _args: argparse.Namespace) -> int:
53
- """
54
- A handler for printing the version of the tool to the console.
55
-
56
- Parameters
57
- ----------
58
- _args : argparse.Namespace
59
- Unused.
60
-
61
- Return
62
- ------
63
- int
64
- Zero when printing to console succeeded.
65
- """
66
- print(__version__)
67
- return 0
68
-
69
52
  def do_it(self) -> int:
70
53
  """
71
54
  Executes the requested command.
@@ -90,6 +73,9 @@ class CLIHandler:
90
73
  file=sys.stderr,
91
74
  )
92
75
  print("Check your arguments and credentials.", file=sys.stderr)
76
+ except ValueError as e:
77
+ print("Whopsie! Something did go wrong.", file=sys.stderr)
78
+ print(e, file=sys.stderr)
93
79
  return ret
94
80
 
95
81
  def _register_common_arguments(self, parser: argparse.ArgumentParser) -> None:
@@ -107,7 +93,7 @@ class CLIHandler:
107
93
  parser:
108
94
  The argparser where to register the common arguments.
109
95
  """
110
- group = parser.add_mutually_exclusive_group()
96
+ group = parser.add_argument_group()
111
97
  group.add_argument(
112
98
  "-H",
113
99
  "--host",
@@ -166,6 +152,12 @@ class CLIHandler:
166
152
  """
167
153
  self._register_common_arguments(parser)
168
154
  parser.add_argument("-v", "--version", type=str, help="The package version.")
155
+ parser.add_argument(
156
+ "--from-file",
157
+ type=str,
158
+ help="Download all files from packages listed in the file. Each line in the "
159
+ "file should follow format <package-name>==<package-version>@<project>.",
160
+ )
169
161
  parser.add_argument(
170
162
  "-f",
171
163
  "--file",
@@ -213,6 +205,8 @@ class CLIHandler:
213
205
  project = os.environ["CI_PROJECT_ID"]
214
206
  token = os.environ["CI_JOB_TOKEN"]
215
207
  token_user = "JOB-TOKEN"
208
+ if args.host:
209
+ host = args.host
216
210
  if args.project:
217
211
  project = args.project
218
212
  if args.token:
@@ -247,23 +241,67 @@ class CLIHandler:
247
241
  host, project, name, token_user, token = self._args(args)
248
242
  version = args.version
249
243
  destination = args.destination
244
+ file = args.file
245
+ from_file = args.from_file
250
246
  packages = Packages(host, token_user, token)
251
- package_id = packages.get_id(project, name, version)
252
- if package_id:
253
- files = []
254
- if args.file:
255
- files.append(args.file)
256
- else:
257
- files = packages.get_files(project, package_id).keys()
258
- for file in files:
259
- ret = packages.download_file(project, name, version, file, destination)
247
+ if from_file:
248
+ package_list = self._read_from_file(from_file)
249
+ for package in package_list:
250
+ ret = packages.download(
251
+ package["project"], package["name"], package["version"], destination
252
+ )
260
253
  if ret:
261
- print("Failed to download file " + file)
262
254
  break
255
+ elif file:
256
+ ret = packages.download_file(project, name, version, file, destination)
263
257
  else:
264
- print("No package " + name + " version " + version + " found!")
258
+ ret = packages.download(project, name, version, destination)
259
+ if ret:
260
+ if ret == 2:
261
+ print("No package " + name + " version " + version + " found!")
262
+ else:
263
+ print("Failed to download file(s)!")
265
264
  return ret
266
265
 
266
+ def _read_from_file(self, filepath: str) -> list[dict]:
267
+ """
268
+ Reads packages file and returns a list of packages to be downloaded.
269
+
270
+ The lines in the file should be
271
+ <packagename>==<version>@<project>
272
+
273
+ Parameters
274
+ ----------
275
+ filepath : str
276
+ Filepath of the packages file
277
+
278
+ Returns
279
+ -------
280
+ int
281
+ Zero if everything goes well, non-zero otherwise
282
+ """
283
+ packages = []
284
+ pattern = (
285
+ r"\s*(?P<packagename>\S+)\s*=="
286
+ r"\s*(?P<packageversion>\S+)\s*@"
287
+ r"\s*(?P<project>\S+)\s*"
288
+ )
289
+ with open(filepath, "r", encoding="utf-8") as file:
290
+ for line in file:
291
+ if not line:
292
+ continue
293
+ match = re.match(pattern, line)
294
+ if not match:
295
+ raise ValueError(
296
+ f"Package file {filepath} line {line} seems fishy! \
297
+ Check that the line is <packagename>==<version>@<project>"
298
+ )
299
+ name = match.group("packagename")
300
+ version = match.group("packageversion")
301
+ project = match.group("project")
302
+ packages.append({"name": name, "version": version, "project": project})
303
+ return packages
304
+
267
305
  def _register_list_parser(self, parser: argparse.ArgumentParser):
268
306
  """
269
307
  Registers the list command related arguments to the parser:
@@ -352,19 +390,21 @@ class CLIHandler:
352
390
  int
353
391
  Zero if everything went fine, non-zero otherwise.
354
392
  """
355
- ret = 0
393
+ ret = 1
356
394
  host, project, name, token_user, token = self._args(args)
357
395
  version = args.version
358
396
  file = args.file
359
397
  source = args.source
398
+ packages = Packages(host, token_user, token)
360
399
  if file:
361
400
  # Check if the uploaded file exists.
362
401
  if not os.path.isfile(os.path.join(source, file)):
363
402
  print("File " + file + " does not exist!")
364
403
  ret = 1
365
- if not ret:
366
- packages = Packages(host, token_user, token)
367
- ret = packages.upload_file(project, name, version, file, source)
404
+ else:
405
+ ret = packages.upload_file(project, name, version, file, source)
406
+ else:
407
+ ret = packages.upload(project, name, version, source)
368
408
  return ret
369
409
 
370
410
  def _register_delete_parser(self, parser: argparse.ArgumentParser):
@@ -415,5 +455,5 @@ class CLIHandler:
415
455
  if file:
416
456
  ret = packages.delete_file(project, name, version, file)
417
457
  else:
418
- ret = packages.delete_package(project, name, version)
458
+ ret = packages.delete(project, name, version)
419
459
  return ret
@@ -0,0 +1,248 @@
1
+ """GitLab basic API"""
2
+
3
+ from http.client import HTTPMessage
4
+ from json import loads
5
+ import logging
6
+ from urllib import parse
7
+ from gitlab.lib import http
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class GitLabAPI:
13
+ """Class for basic GitLab API"""
14
+
15
+ def __init__(self, host: str, token_type: str, token: str):
16
+ """
17
+ Creates a new instance of class.
18
+
19
+ Parameters
20
+ ----------
21
+ host : str
22
+ The GitLab instance hostname, without schema.
23
+ The host will be used for the package API interaction.
24
+ For example gitlab.com.
25
+ token_type : str
26
+ The token type or "user" to authenticate with GitLab REST API.
27
+ For personal, project, and group tokens this is `PRIVATE-TOKEN`.
28
+ For `CI_JOB_TOKEN` this is `JOB-TOKEN`.
29
+ Can be left empty when authentication is not used.
30
+ token : str
31
+ The token (secret) to authenticate with GitLab REST API.
32
+ This can be a personal token, project token, or`CI_JOB_TOKEN`.
33
+ Leave empty when authentication is not used.
34
+ """
35
+ self.host = host
36
+ self.token_type = token_type
37
+ self.token = token
38
+
39
+ def _url(self) -> str:
40
+ """
41
+ Returns the GitLab REST API URL by using the host variable.
42
+
43
+ Returns
44
+ -------
45
+ str
46
+ The GitLab REST API URL, for example `https://gitlab.com/api/v4/`.
47
+ """
48
+ return f"https://{self.host}/api/v4/"
49
+
50
+ def _get_headers(self) -> dict:
51
+ """
52
+ Creates headers for a GitLab REST API call.
53
+
54
+ The headers contain token for authentication according to the
55
+ instance variables.
56
+
57
+ Returns
58
+ -------
59
+ dict
60
+ Headers for a REST API request, that contain the authentication token.
61
+ """
62
+ headers = {}
63
+ if self.token_type and self.token:
64
+ headers = {self.token_type: self.token}
65
+ return headers
66
+
67
+ def _parse_header_links(self, headers: HTTPMessage) -> dict:
68
+ """
69
+ Parses link field from HTTP headers to a dictionary where
70
+ the link "rel" value is the key.
71
+
72
+ This is useful for example with GitLab REST API to get the pagination links.
73
+
74
+ Parameters
75
+ ----------
76
+ headers : HTTPMessage
77
+ The HTTP response headers that contain the links
78
+
79
+ Returns
80
+ -------
81
+ dict
82
+ The header links in a dictionary, that can be used to for example pagination:
83
+ _parse_header_links(headers).get("next") returns the next page link, or None.
84
+ """
85
+ links = {}
86
+ header_links = headers.get("link")
87
+ if header_links:
88
+ items = header_links.split(",")
89
+ # Values are <uri-reference>; param1=value1; param2="value2"
90
+ for item in items:
91
+ parts = item.split(";", 1)
92
+ if parts:
93
+ # First value should be the URI
94
+ val = parts.pop(0).lstrip(" <").rstrip("> ")
95
+ # The rest are the parameters; let's take the first one that has rel=
96
+ # in it, split it with ; and take the first part as a value
97
+ typ = parts.pop()
98
+ typ = typ.split("rel=", 1).pop().split(";").pop(0).strip('" ')
99
+ links[typ] = val
100
+ return links
101
+
102
+ def _build_query(self, **args: str) -> str:
103
+ """
104
+ Builds a query for a GitLab REST API request
105
+
106
+ Parameters
107
+ ----------
108
+ args : str
109
+ keyword arguments for the REST API query, like per_page=20, page=20
110
+
111
+ Returns
112
+ -------
113
+ str
114
+ A query string for a REST API request. Append this to
115
+ the request URL. Example `?per_page=20&page=20`.
116
+ """
117
+ query = ""
118
+ for key, value in args.items():
119
+ query = "&".join(filter(None, (query, f"{key}={parse.quote_plus(value)}")))
120
+ if query:
121
+ query = "?" + query
122
+ return query
123
+
124
+ def _build_path(self, *paths: str) -> str:
125
+ """
126
+ Returns the path to be appended to the REST API URL
127
+
128
+ Parameters
129
+ ----------
130
+ paths : str
131
+ The path string that are joined with "/".
132
+
133
+ Returns
134
+ -------
135
+ str
136
+ A URL path, for example `projects/123/`
137
+ or `projects/namespace%2Fproject/`
138
+ """
139
+ quoted_paths = []
140
+ for subpath in paths:
141
+ quoted_paths.append(subpath)
142
+ return "/".join(quoted_paths)
143
+
144
+ def get(self, *paths: str, **query_params: str) -> tuple[int, bytes, HTTPMessage]:
145
+ """
146
+ Makes a HTTP GET request to the given GitLab path, and returns
147
+ the response status, body, and headers.
148
+
149
+ Parameters
150
+ ----------
151
+ paths : str
152
+ The URL path of the HTTP request to make.
153
+ query_params : str, optional
154
+ Dictionary of query parameters for the request, like package_name=mypackage
155
+
156
+ Returns
157
+ -------
158
+ int
159
+ The HTTP response code, such as 200
160
+ bytes
161
+ The HTTP response body read as bytes
162
+ HTTPMessage
163
+ The HTTP response headers
164
+ """
165
+ url = self._url() + self._build_path(*paths) + self._build_query(**query_params)
166
+ headers = self._get_headers()
167
+ return http.get(url, headers)
168
+
169
+ def get_all(self, *paths: str, **query_params: str) -> list:
170
+ """
171
+ Returns data from the REST API endpoint. In case
172
+ of multiple pages, all data will be returned.
173
+
174
+ Parameters
175
+ ----------
176
+ paths : str
177
+ The paths of the API endpoint that is called. For example projects, 123, packages
178
+ would be querying the projects/123/packages endpoint.
179
+ query_params : str, optional
180
+ Additional arguments for the query of the URL, for example to filter
181
+ results: package_name=mypackage
182
+
183
+ Returns
184
+ -------
185
+ list
186
+ Data from GitLab REST API endpoint with the arguments.
187
+ """
188
+ url = self._url() + self._build_path(*paths) + self._build_query(**query_params)
189
+ data = []
190
+ while url:
191
+ res_status, res_data, res_headers = http.get(url, self._get_headers())
192
+ logger.debug("Response status: %d", res_status)
193
+ res_data = loads(res_data)
194
+ logger.debug("Response data: %s", res_data)
195
+ data = data + res_data
196
+ url = self._parse_header_links(res_headers).get("next")
197
+ return data
198
+
199
+ def put(self, data: bytes, *paths: str, **query_params: str) -> int:
200
+ """
201
+ Makes a HTTP PUT request to the given GitLab path.
202
+
203
+ Parameters
204
+ ----------
205
+ data : bytes
206
+ The data to PUT.
207
+ paths : str
208
+ The URL path of the HTTP request to make.
209
+ query_params : str, optional
210
+ Additional arguments for the query of the URL, for example to filter
211
+ results: package_name=mypackage
212
+
213
+ Returns
214
+ -------
215
+ int
216
+ 0 If the request was successful.
217
+ """
218
+ ret = 1
219
+ url = self._url() + self._build_path(*paths) + self._build_query(**query_params)
220
+ status, _, _ = http.put(url, data, self._get_headers())
221
+ if status == 201: # 201 is created
222
+ ret = 0
223
+ return ret
224
+
225
+ def delete(self, *paths: str, **query_params: str) -> int:
226
+ """
227
+ Makes a HTTP DELETE request to the given GitLab path.
228
+
229
+ Parameters
230
+ ----------
231
+ paths : str
232
+ The URL path of the HTTP request to make.
233
+ query_params : str, optional
234
+ Additional arguments for the query of the URL, for example to filter
235
+ results: package_name=mypackage
236
+
237
+ Returns
238
+ -------
239
+ int
240
+ 0 If the request was successful.
241
+ """
242
+ ret = 1
243
+ url = self._url() + self._build_path(*paths) + self._build_query(**query_params)
244
+ status, _, _ = http.delete(url, self._get_headers())
245
+ if status == 204:
246
+ # 204 is no content, that GL responds when file deleted
247
+ ret = 0
248
+ return ret
@@ -0,0 +1,90 @@
1
+ """HTTP helper"""
2
+
3
+ from http.client import HTTPMessage
4
+ import logging
5
+ from urllib import request
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ def get(url: str, headers: dict) -> tuple[int, bytes, HTTPMessage]:
11
+ """
12
+ Makes a raw GET request to the given URL, and returns
13
+ the response status, body, and headers.
14
+
15
+ Parameters
16
+ ----------
17
+ url : str
18
+ The URL of the HTTP request to make.
19
+ headers: dict
20
+ The HTTP headers used in the request.
21
+
22
+ Returns
23
+ -------
24
+ int
25
+ The HTTP response code, such as 200
26
+ bytes
27
+ The HTTP response body read as bytes
28
+ HTTPMessage
29
+ The HTTP response headers
30
+ """
31
+ logger.debug("Getting %s", url)
32
+ req = request.Request(url, headers=headers)
33
+ with request.urlopen(req) as response:
34
+ return response.status, response.read(), response.headers
35
+
36
+
37
+ def put(url: str, data: bytes, headers: dict) -> tuple[int, bytes, HTTPMessage]:
38
+ """
39
+ Makes a raw PUT request to the given URL, and returns
40
+ the response status, body, and headers.
41
+
42
+ Parameters
43
+ ----------
44
+ url : str
45
+ The URL of the HTTP request to make.
46
+ data : bytes
47
+ The data to PUT
48
+ headers: dict
49
+ The HTTP headers used in the request.
50
+
51
+ Returns
52
+ -------
53
+ int
54
+ The HTTP response code, such as 200
55
+ bytes
56
+ The HTTP response body read as bytes
57
+ HTTPMessage
58
+ The HTTP response headers
59
+ """
60
+ logger.debug("Putting %s", url)
61
+ req = request.Request(url, method="PUT", data=data, headers=headers)
62
+ with request.urlopen(req) as response:
63
+ return response.status, response.read(), response.headers
64
+
65
+
66
+ def delete(url, headers):
67
+ """
68
+ Makes a raw DELETE request to the given URL, and returns
69
+ the response status, body, and headers.
70
+
71
+ Parameters
72
+ ----------
73
+ url : str
74
+ The URL of the HTTP request to make.
75
+ headers: dict
76
+ The HTTP headers used in the request.
77
+
78
+ Returns
79
+ -------
80
+ int
81
+ The HTTP response code, such as 200
82
+ bytes
83
+ The HTTP response body read as bytes
84
+ HTTPMessage
85
+ The HTTP response headers
86
+ """
87
+ logger.debug("Deleting %s", url)
88
+ req = request.Request(url, method="DELETE", headers=headers)
89
+ with request.urlopen(req) as response:
90
+ return response.status, response.read(), response.headers