dt-extensions-sdk 1.1.23__py3-none-any.whl → 1.2.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.
Files changed (32) hide show
  1. {dt_extensions_sdk-1.1.23.dist-info → dt_extensions_sdk-1.2.0.dist-info}/METADATA +2 -2
  2. dt_extensions_sdk-1.2.0.dist-info/RECORD +34 -0
  3. {dt_extensions_sdk-1.1.23.dist-info → dt_extensions_sdk-1.2.0.dist-info}/WHEEL +1 -1
  4. {dt_extensions_sdk-1.1.23.dist-info → dt_extensions_sdk-1.2.0.dist-info}/licenses/LICENSE.txt +9 -9
  5. dynatrace_extension/__about__.py +5 -5
  6. dynatrace_extension/__init__.py +27 -27
  7. dynatrace_extension/cli/__init__.py +5 -5
  8. dynatrace_extension/cli/create/__init__.py +1 -1
  9. dynatrace_extension/cli/create/create.py +76 -76
  10. dynatrace_extension/cli/create/extension_template/.gitignore.template +160 -160
  11. dynatrace_extension/cli/create/extension_template/README.md.template +33 -33
  12. dynatrace_extension/cli/create/extension_template/activation.json.template +15 -15
  13. dynatrace_extension/cli/create/extension_template/extension/activationSchema.json.template +118 -118
  14. dynatrace_extension/cli/create/extension_template/extension/extension.yaml.template +17 -17
  15. dynatrace_extension/cli/create/extension_template/extension_name/__main__.py.template +43 -43
  16. dynatrace_extension/cli/create/extension_template/setup.py.template +28 -28
  17. dynatrace_extension/cli/main.py +432 -428
  18. dynatrace_extension/cli/schema.py +129 -129
  19. dynatrace_extension/sdk/__init__.py +3 -3
  20. dynatrace_extension/sdk/activation.py +43 -43
  21. dynatrace_extension/sdk/callback.py +134 -134
  22. dynatrace_extension/sdk/communication.py +483 -482
  23. dynatrace_extension/sdk/event.py +19 -19
  24. dynatrace_extension/sdk/extension.py +1065 -1045
  25. dynatrace_extension/sdk/helper.py +191 -191
  26. dynatrace_extension/sdk/metric.py +118 -118
  27. dynatrace_extension/sdk/runtime.py +67 -67
  28. dynatrace_extension/sdk/snapshot.py +198 -0
  29. dynatrace_extension/sdk/vendor/mureq/LICENSE +13 -13
  30. dynatrace_extension/sdk/vendor/mureq/mureq.py +448 -447
  31. dt_extensions_sdk-1.1.23.dist-info/RECORD +0 -33
  32. {dt_extensions_sdk-1.1.23.dist-info → dt_extensions_sdk-1.2.0.dist-info}/entry_points.txt +0 -0
@@ -1,447 +1,448 @@
1
- """
2
- mureq is a replacement for python-requests, intended to be vendored
3
- in-tree by Linux systems software and other lightweight applications.
4
- mureq is copyright 2021 by its contributors and is released under the
5
- 0BSD ("zero-clause BSD") license.
6
- """
7
- import contextlib
8
- import io
9
- import os.path
10
- import socket
11
- import ssl
12
- import sys
13
- import urllib.parse
14
- from http.client import HTTPConnection, HTTPException, HTTPMessage, HTTPSConnection
15
-
16
- __version__ = "0.2.0"
17
-
18
- __all__ = [
19
- "HTTPException",
20
- "TooManyRedirects",
21
- "Response",
22
- "yield_response",
23
- "request",
24
- "get",
25
- "post",
26
- "head",
27
- "put",
28
- "patch",
29
- "delete",
30
- ]
31
-
32
- DEFAULT_TIMEOUT = 15.0
33
-
34
- # e.g. "Python 3.8.10"
35
- DEFAULT_UA = "Python " + sys.version.split()[0]
36
-
37
-
38
- def request(method, url, *, read_limit=None, **kwargs):
39
- """request performs an HTTP request and reads the entire response body.
40
- :param str method: HTTP method to request (e.g. 'GET', 'POST')
41
- :param str url: URL to request
42
- :param read_limit: maximum number of bytes to read from the body, or None for no limit
43
- :type read_limit: int or None
44
- :param kwargs: optional arguments defined by yield_response
45
- :return: Response object
46
- :rtype: Response
47
- :raises: HTTPException
48
- """
49
- with yield_response(method, url, **kwargs) as response:
50
- try:
51
- body = response.read(read_limit)
52
- except HTTPException:
53
- raise
54
- except OSError as e:
55
- raise HTTPException(str(e)) from e
56
- return Response(response.url, response.status, _prepare_incoming_headers(response.headers), body)
57
-
58
-
59
- def get(url, **kwargs):
60
- """get performs an HTTP GET request."""
61
- return request("GET", url=url, **kwargs)
62
-
63
-
64
- def post(url, body=None, **kwargs):
65
- """post performs an HTTP POST request."""
66
- return request("POST", url=url, body=body, **kwargs)
67
-
68
-
69
- def head(url, **kwargs):
70
- """head performs an HTTP HEAD request."""
71
- return request("HEAD", url=url, **kwargs)
72
-
73
-
74
- def put(url, body=None, **kwargs):
75
- """put performs an HTTP PUT request."""
76
- return request("PUT", url=url, body=body, **kwargs)
77
-
78
-
79
- def patch(url, body=None, **kwargs):
80
- """patch performs an HTTP PATCH request."""
81
- return request("PATCH", url=url, body=body, **kwargs)
82
-
83
-
84
- def delete(url, **kwargs):
85
- """delete performs an HTTP DELETE request."""
86
- return request("DELETE", url=url, **kwargs)
87
-
88
-
89
- @contextlib.contextmanager
90
- def yield_response(
91
- method,
92
- url,
93
- *,
94
- unix_socket=None,
95
- timeout=DEFAULT_TIMEOUT,
96
- headers=None,
97
- params=None,
98
- body=None,
99
- form=None,
100
- json=None,
101
- verify=True,
102
- source_address=None,
103
- max_redirects=None,
104
- ssl_context=None,
105
- ):
106
- """yield_response is a low-level API that exposes the actual
107
- http.client.HTTPResponse via a contextmanager.
108
- Note that unlike mureq.Response, http.client.HTTPResponse does not
109
- automatically canonicalize multiple appearances of the same header by
110
- joining them together with a comma delimiter. To retrieve canonicalized
111
- headers from the response, use response.getheader():
112
- https://docs.python.org/3/library/http.client.html#http.client.HTTPResponse.getheader
113
- :param str method: HTTP method to request (e.g. 'GET', 'POST')
114
- :param str url: URL to request
115
- :param unix_socket: path to Unix domain socket to query, or None for a normal TCP request
116
- :type unix_socket: str or None
117
- :param timeout: timeout in seconds, or None for no timeout (default: 15 seconds)
118
- :type timeout: float or None
119
- :param headers: HTTP headers as a mapping or list of key-value pairs
120
- :param params: parameters to be URL-encoded and added to the query string, as a mapping or list of key-value pairs
121
- :param body: payload body of the request
122
- :type body: bytes or None
123
- :param form: parameters to be form-encoded and sent as the payload body, as a mapping or list of key-value pairs
124
- :param json: object to be serialized as JSON and sent as the payload body
125
- :param bool verify: whether to verify TLS certificates (default: True)
126
- :param source_address: source address to bind to for TCP
127
- :type source_address: str or tuple(str, int) or None
128
- :param max_redirects: maximum number of redirects to follow, or None (the default) for no redirection
129
- :type max_redirects: int or None
130
- :param ssl_context: TLS config to control certificate validation, or None for default behavior
131
- :type ssl_context: ssl.SSLContext or None
132
- :return: http.client.HTTPResponse, yielded as context manager
133
- :rtype: http.client.HTTPResponse
134
- :raises: HTTPException
135
- """
136
- method = method.upper()
137
- headers = _prepare_outgoing_headers(headers)
138
- enc_params = _prepare_params(params)
139
- body = _prepare_body(body, form, json, headers)
140
-
141
- visited_urls = []
142
-
143
- while max_redirects is None or len(visited_urls) <= max_redirects:
144
- url, conn, path = _prepare_request(
145
- method,
146
- url,
147
- enc_params=enc_params,
148
- timeout=timeout,
149
- unix_socket=unix_socket,
150
- verify=verify,
151
- source_address=source_address,
152
- ssl_context=ssl_context,
153
- )
154
- enc_params = "" # don't reappend enc_params if we get redirected
155
- visited_urls.append(url)
156
- try:
157
- try:
158
- conn.request(method, path, headers=headers, body=body)
159
- response = conn.getresponse()
160
- except HTTPException:
161
- raise
162
- except OSError as e:
163
- # wrap any IOError that is not already an HTTPException
164
- # in HTTPException, exposing a uniform API for remote errors
165
- raise HTTPException(str(e)) from e
166
- redirect_url = _check_redirect(url, response.status, response.headers)
167
- if max_redirects is None or redirect_url is None:
168
- response.url = url # https://bugs.python.org/issue42062
169
- yield response
170
- return
171
- else:
172
- url = redirect_url
173
- if response.status == 303:
174
- # 303 See Other: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303
175
- method = "GET"
176
- finally:
177
- conn.close()
178
-
179
- raise TooManyRedirects(visited_urls)
180
-
181
-
182
- class Response:
183
- """Response contains a completely consumed HTTP response.
184
- :ivar str url: the retrieved URL, indicating whether a redirection occurred
185
- :ivar int status_code: the HTTP status code
186
- :ivar http.client.HTTPMessage headers: the HTTP headers
187
- :ivar bytes body: the payload body of the response
188
- """
189
-
190
- __slots__ = ("url", "status_code", "headers", "body")
191
-
192
- def __init__(self, url, status_code, headers, body):
193
- self.url, self.status_code, self.headers, self.body = url, status_code, headers, body
194
-
195
- def __repr__(self):
196
- return f"Response(status_code={self.status_code:d})"
197
-
198
- @property
199
- def ok(self):
200
- """ok returns whether the response had a successful status code
201
- (anything other than a 40x or 50x)."""
202
- return not (400 <= self.status_code < 600)
203
-
204
- @property
205
- def content(self):
206
- """content returns the response body (the `body` member). This is an
207
- alias for compatibility with requests.Response."""
208
- return self.body
209
-
210
- def raise_for_status(self):
211
- """raise_for_status checks the response's success code, raising an
212
- exception for error codes."""
213
- if not self.ok:
214
- raise HTTPErrorStatus(self.status_code)
215
-
216
- def json(self):
217
- """Attempts to deserialize the response body as UTF-8 encoded JSON."""
218
- import json as jsonlib
219
-
220
- return jsonlib.loads(self.body)
221
-
222
- def _debugstr(self):
223
- buf = io.StringIO()
224
- print("HTTP", self.status_code, file=buf)
225
- for k, v in self.headers.items():
226
- print(f"{k}: {v}", file=buf)
227
- print(file=buf)
228
- try:
229
- print(self.body.decode("utf-8"), file=buf)
230
- except UnicodeDecodeError:
231
- print(f"<{len(self.body)} bytes binary data>", file=buf)
232
- return buf.getvalue()
233
-
234
-
235
- class TooManyRedirects(HTTPException):
236
- """TooManyRedirects is raised when automatic following of redirects was
237
- enabled, but the server redirected too many times without completing."""
238
-
239
- pass
240
-
241
-
242
- class HTTPErrorStatus(HTTPException):
243
- """HTTPErrorStatus is raised by Response.raise_for_status() to indicate an
244
- HTTP error code (a 40x or a 50x). Note that a well-formed response with an
245
- error code does not result in an exception unless raise_for_status() is
246
- called explicitly.
247
- """
248
-
249
- def __init__(self, status_code):
250
- self.status_code = status_code
251
-
252
- def __str__(self):
253
- return f"HTTP response returned error code {self.status_code:d}"
254
-
255
-
256
- # end public API, begin internal implementation details
257
-
258
- _JSON_CONTENTTYPE = "application/json"
259
- _FORM_CONTENTTYPE = "application/x-www-form-urlencoded"
260
-
261
-
262
- class UnixHTTPConnection(HTTPConnection):
263
- """UnixHTTPConnection is a subclass of HTTPConnection that connects to a
264
- Unix domain stream socket instead of a TCP address.
265
- """
266
-
267
- def __init__(self, path, timeout=DEFAULT_TIMEOUT):
268
- super(UnixHTTPConnection, self).__init__("localhost", timeout=timeout)
269
- self._unix_path = path
270
-
271
- def connect(self):
272
- sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
273
- try:
274
- sock.settimeout(self.timeout)
275
- sock.connect(self._unix_path)
276
- except Exception:
277
- sock.close()
278
- raise
279
- self.sock = sock
280
-
281
-
282
- def _check_redirect(url, status, response_headers):
283
- """Return the URL to redirect to, or None for no redirection."""
284
- if status not in (301, 302, 303, 307, 308):
285
- return None
286
- location = response_headers.get("Location")
287
- if not location:
288
- return None
289
- parsed_location = urllib.parse.urlparse(location)
290
- if parsed_location.scheme:
291
- # absolute URL
292
- return location
293
-
294
- old_url = urllib.parse.urlparse(url)
295
- if location.startswith("/"):
296
- # absolute path on old hostname
297
- return urllib.parse.urlunparse(
298
- (
299
- old_url.scheme,
300
- old_url.netloc,
301
- parsed_location.path,
302
- parsed_location.params,
303
- parsed_location.query,
304
- parsed_location.fragment,
305
- )
306
- )
307
-
308
- # relative path on old hostname
309
- old_dir, _old_file = os.path.split(old_url.path)
310
- new_path = os.path.join(old_dir, location)
311
- return urllib.parse.urlunparse(
312
- (
313
- old_url.scheme,
314
- old_url.netloc,
315
- new_path,
316
- parsed_location.params,
317
- parsed_location.query,
318
- parsed_location.fragment,
319
- )
320
- )
321
-
322
-
323
- def _prepare_outgoing_headers(headers):
324
- if headers is None:
325
- headers = HTTPMessage()
326
- elif not isinstance(headers, HTTPMessage):
327
- new_headers = HTTPMessage()
328
- if hasattr(headers, "items"):
329
- iterator = headers.items()
330
- else:
331
- iterator = iter(headers)
332
- for k, v in iterator:
333
- new_headers[k] = v
334
- headers = new_headers
335
- _setdefault_header(headers, "User-Agent", DEFAULT_UA)
336
- return headers
337
-
338
-
339
- # XXX join multi-headers together so that get(), __getitem__(),
340
- # etc. behave intuitively, then stuff them back in an HTTPMessage.
341
- def _prepare_incoming_headers(headers):
342
- headers_dict = {}
343
- for k, v in headers.items():
344
- headers_dict.setdefault(k, []).append(v)
345
- result = HTTPMessage()
346
- # note that iterating over headers_dict preserves the original
347
- # insertion order in all versions since Python 3.6:
348
- for k, vlist in headers_dict.items():
349
- result[k] = ",".join(vlist)
350
- return result
351
-
352
-
353
- def _setdefault_header(headers, name, value):
354
- if name not in headers:
355
- headers[name] = value
356
-
357
-
358
- def _prepare_body(body, form, json, headers):
359
- if body is not None:
360
- if not isinstance(body, bytes):
361
- raise TypeError("body must be bytes or None", type(body))
362
- return body
363
-
364
- if json is not None:
365
- _setdefault_header(headers, "Content-Type", _JSON_CONTENTTYPE)
366
- import json as jsonlib
367
-
368
- return jsonlib.dumps(json).encode("utf-8")
369
-
370
- if form is not None:
371
- _setdefault_header(headers, "Content-Type", _FORM_CONTENTTYPE)
372
- return urllib.parse.urlencode(form, doseq=True)
373
-
374
- return None
375
-
376
-
377
- def _prepare_params(params):
378
- if params is None:
379
- return ""
380
- return urllib.parse.urlencode(params, doseq=True)
381
-
382
-
383
- def _prepare_request(
384
- method,
385
- url,
386
- *,
387
- enc_params="",
388
- timeout=DEFAULT_TIMEOUT,
389
- source_address=None,
390
- unix_socket=None,
391
- verify=True,
392
- ssl_context=None,
393
- ):
394
- """Parses the URL, returns the path and the right HTTPConnection subclass."""
395
- parsed_url = urllib.parse.urlparse(url)
396
-
397
- is_unix = unix_socket is not None
398
- scheme = parsed_url.scheme.lower()
399
- if scheme.endswith("+unix"):
400
- scheme = scheme[:-5]
401
- is_unix = True
402
- if scheme == "https":
403
- raise ValueError("https+unix is not implemented")
404
-
405
- if scheme not in ("http", "https"):
406
- raise ValueError("unrecognized scheme", scheme)
407
-
408
- is_https = scheme == "https"
409
- host = parsed_url.hostname
410
- port = 443 if is_https else 80
411
- if parsed_url.port:
412
- port = parsed_url.port
413
-
414
- if is_unix and unix_socket is None:
415
- unix_socket = urllib.parse.unquote(parsed_url.netloc)
416
-
417
- path = parsed_url.path
418
- if parsed_url.query:
419
- if enc_params:
420
- path = f"{path}?{parsed_url.query}&{enc_params}"
421
- else:
422
- path = f"{path}?{parsed_url.query}"
423
- else:
424
- if enc_params:
425
- path = f"{path}?{enc_params}"
426
- else:
427
- pass # just parsed_url.path in this case
428
-
429
- if isinstance(source_address, str):
430
- source_address = (source_address, 0)
431
-
432
- if is_unix:
433
- conn = UnixHTTPConnection(unix_socket, timeout=timeout)
434
- elif is_https:
435
- if ssl_context is None:
436
- ssl_context = ssl.create_default_context()
437
- if not verify:
438
- ssl_context.check_hostname = False
439
- ssl_context.verify_mode = ssl.CERT_NONE
440
- conn = HTTPSConnection(host, port, source_address=source_address, timeout=timeout, context=ssl_context)
441
- else:
442
- conn = HTTPConnection(host, port, source_address=source_address, timeout=timeout)
443
-
444
- munged_url = urllib.parse.urlunparse(
445
- (parsed_url.scheme, parsed_url.netloc, path, parsed_url.params, "", parsed_url.fragment)
446
- )
447
- return munged_url, conn, path
1
+ """
2
+ mureq is a replacement for python-requests, intended to be vendored
3
+ in-tree by Linux systems software and other lightweight applications.
4
+ mureq is copyright 2021 by its contributors and is released under the
5
+ 0BSD ("zero-clause BSD") license.
6
+ """
7
+
8
+ import contextlib
9
+ import io
10
+ import os.path
11
+ import socket
12
+ import ssl
13
+ import sys
14
+ import urllib.parse
15
+ from http.client import HTTPConnection, HTTPException, HTTPMessage, HTTPSConnection
16
+
17
+ __version__ = "0.2.0"
18
+
19
+ __all__ = [
20
+ "HTTPException",
21
+ "TooManyRedirects",
22
+ "Response",
23
+ "yield_response",
24
+ "request",
25
+ "get",
26
+ "post",
27
+ "head",
28
+ "put",
29
+ "patch",
30
+ "delete",
31
+ ]
32
+
33
+ DEFAULT_TIMEOUT = 15.0
34
+
35
+ # e.g. "Python 3.8.10"
36
+ DEFAULT_UA = "Python " + sys.version.split()[0]
37
+
38
+
39
+ def request(method, url, *, read_limit=None, **kwargs):
40
+ """request performs an HTTP request and reads the entire response body.
41
+ :param str method: HTTP method to request (e.g. 'GET', 'POST')
42
+ :param str url: URL to request
43
+ :param read_limit: maximum number of bytes to read from the body, or None for no limit
44
+ :type read_limit: int or None
45
+ :param kwargs: optional arguments defined by yield_response
46
+ :return: Response object
47
+ :rtype: Response
48
+ :raises: HTTPException
49
+ """
50
+ with yield_response(method, url, **kwargs) as response:
51
+ try:
52
+ body = response.read(read_limit)
53
+ except HTTPException:
54
+ raise
55
+ except OSError as e:
56
+ raise HTTPException(str(e)) from e
57
+ return Response(response.url, response.status, _prepare_incoming_headers(response.headers), body)
58
+
59
+
60
+ def get(url, **kwargs):
61
+ """get performs an HTTP GET request."""
62
+ return request("GET", url=url, **kwargs)
63
+
64
+
65
+ def post(url, body=None, **kwargs):
66
+ """post performs an HTTP POST request."""
67
+ return request("POST", url=url, body=body, **kwargs)
68
+
69
+
70
+ def head(url, **kwargs):
71
+ """head performs an HTTP HEAD request."""
72
+ return request("HEAD", url=url, **kwargs)
73
+
74
+
75
+ def put(url, body=None, **kwargs):
76
+ """put performs an HTTP PUT request."""
77
+ return request("PUT", url=url, body=body, **kwargs)
78
+
79
+
80
+ def patch(url, body=None, **kwargs):
81
+ """patch performs an HTTP PATCH request."""
82
+ return request("PATCH", url=url, body=body, **kwargs)
83
+
84
+
85
+ def delete(url, **kwargs):
86
+ """delete performs an HTTP DELETE request."""
87
+ return request("DELETE", url=url, **kwargs)
88
+
89
+
90
+ @contextlib.contextmanager
91
+ def yield_response(
92
+ method,
93
+ url,
94
+ *,
95
+ unix_socket=None,
96
+ timeout=DEFAULT_TIMEOUT,
97
+ headers=None,
98
+ params=None,
99
+ body=None,
100
+ form=None,
101
+ json=None,
102
+ verify=True,
103
+ source_address=None,
104
+ max_redirects=None,
105
+ ssl_context=None,
106
+ ):
107
+ """yield_response is a low-level API that exposes the actual
108
+ http.client.HTTPResponse via a contextmanager.
109
+ Note that unlike mureq.Response, http.client.HTTPResponse does not
110
+ automatically canonicalize multiple appearances of the same header by
111
+ joining them together with a comma delimiter. To retrieve canonicalized
112
+ headers from the response, use response.getheader():
113
+ https://docs.python.org/3/library/http.client.html#http.client.HTTPResponse.getheader
114
+ :param str method: HTTP method to request (e.g. 'GET', 'POST')
115
+ :param str url: URL to request
116
+ :param unix_socket: path to Unix domain socket to query, or None for a normal TCP request
117
+ :type unix_socket: str or None
118
+ :param timeout: timeout in seconds, or None for no timeout (default: 15 seconds)
119
+ :type timeout: float or None
120
+ :param headers: HTTP headers as a mapping or list of key-value pairs
121
+ :param params: parameters to be URL-encoded and added to the query string, as a mapping or list of key-value pairs
122
+ :param body: payload body of the request
123
+ :type body: bytes or None
124
+ :param form: parameters to be form-encoded and sent as the payload body, as a mapping or list of key-value pairs
125
+ :param json: object to be serialized as JSON and sent as the payload body
126
+ :param bool verify: whether to verify TLS certificates (default: True)
127
+ :param source_address: source address to bind to for TCP
128
+ :type source_address: str or tuple(str, int) or None
129
+ :param max_redirects: maximum number of redirects to follow, or None (the default) for no redirection
130
+ :type max_redirects: int or None
131
+ :param ssl_context: TLS config to control certificate validation, or None for default behavior
132
+ :type ssl_context: ssl.SSLContext or None
133
+ :return: http.client.HTTPResponse, yielded as context manager
134
+ :rtype: http.client.HTTPResponse
135
+ :raises: HTTPException
136
+ """
137
+ method = method.upper()
138
+ headers = _prepare_outgoing_headers(headers)
139
+ enc_params = _prepare_params(params)
140
+ body = _prepare_body(body, form, json, headers)
141
+
142
+ visited_urls = []
143
+
144
+ while max_redirects is None or len(visited_urls) <= max_redirects:
145
+ url, conn, path = _prepare_request(
146
+ method,
147
+ url,
148
+ enc_params=enc_params,
149
+ timeout=timeout,
150
+ unix_socket=unix_socket,
151
+ verify=verify,
152
+ source_address=source_address,
153
+ ssl_context=ssl_context,
154
+ )
155
+ enc_params = "" # don't reappend enc_params if we get redirected
156
+ visited_urls.append(url)
157
+ try:
158
+ try:
159
+ conn.request(method, path, headers=headers, body=body)
160
+ response = conn.getresponse()
161
+ except HTTPException:
162
+ raise
163
+ except OSError as e:
164
+ # wrap any IOError that is not already an HTTPException
165
+ # in HTTPException, exposing a uniform API for remote errors
166
+ raise HTTPException(str(e)) from e
167
+ redirect_url = _check_redirect(url, response.status, response.headers)
168
+ if max_redirects is None or redirect_url is None:
169
+ response.url = url # https://bugs.python.org/issue42062
170
+ yield response
171
+ return
172
+ else:
173
+ url = redirect_url
174
+ if response.status == 303:
175
+ # 303 See Other: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303
176
+ method = "GET"
177
+ finally:
178
+ conn.close()
179
+
180
+ raise TooManyRedirects(visited_urls)
181
+
182
+
183
+ class Response:
184
+ """Response contains a completely consumed HTTP response.
185
+ :ivar str url: the retrieved URL, indicating whether a redirection occurred
186
+ :ivar int status_code: the HTTP status code
187
+ :ivar http.client.HTTPMessage headers: the HTTP headers
188
+ :ivar bytes body: the payload body of the response
189
+ """
190
+
191
+ __slots__ = ("url", "status_code", "headers", "body")
192
+
193
+ def __init__(self, url, status_code, headers, body):
194
+ self.url, self.status_code, self.headers, self.body = url, status_code, headers, body
195
+
196
+ def __repr__(self):
197
+ return f"Response(status_code={self.status_code:d})"
198
+
199
+ @property
200
+ def ok(self):
201
+ """ok returns whether the response had a successful status code
202
+ (anything other than a 40x or 50x)."""
203
+ return not (400 <= self.status_code < 600)
204
+
205
+ @property
206
+ def content(self):
207
+ """content returns the response body (the `body` member). This is an
208
+ alias for compatibility with requests.Response."""
209
+ return self.body
210
+
211
+ def raise_for_status(self):
212
+ """raise_for_status checks the response's success code, raising an
213
+ exception for error codes."""
214
+ if not self.ok:
215
+ raise HTTPErrorStatus(self.status_code)
216
+
217
+ def json(self):
218
+ """Attempts to deserialize the response body as UTF-8 encoded JSON."""
219
+ import json as jsonlib
220
+
221
+ return jsonlib.loads(self.body)
222
+
223
+ def _debugstr(self):
224
+ buf = io.StringIO()
225
+ print("HTTP", self.status_code, file=buf)
226
+ for k, v in self.headers.items():
227
+ print(f"{k}: {v}", file=buf)
228
+ print(file=buf)
229
+ try:
230
+ print(self.body.decode("utf-8"), file=buf)
231
+ except UnicodeDecodeError:
232
+ print(f"<{len(self.body)} bytes binary data>", file=buf)
233
+ return buf.getvalue()
234
+
235
+
236
+ class TooManyRedirects(HTTPException):
237
+ """TooManyRedirects is raised when automatic following of redirects was
238
+ enabled, but the server redirected too many times without completing."""
239
+
240
+ pass
241
+
242
+
243
+ class HTTPErrorStatus(HTTPException):
244
+ """HTTPErrorStatus is raised by Response.raise_for_status() to indicate an
245
+ HTTP error code (a 40x or a 50x). Note that a well-formed response with an
246
+ error code does not result in an exception unless raise_for_status() is
247
+ called explicitly.
248
+ """
249
+
250
+ def __init__(self, status_code):
251
+ self.status_code = status_code
252
+
253
+ def __str__(self):
254
+ return f"HTTP response returned error code {self.status_code:d}"
255
+
256
+
257
+ # end public API, begin internal implementation details
258
+
259
+ _JSON_CONTENTTYPE = "application/json"
260
+ _FORM_CONTENTTYPE = "application/x-www-form-urlencoded"
261
+
262
+
263
+ class UnixHTTPConnection(HTTPConnection):
264
+ """UnixHTTPConnection is a subclass of HTTPConnection that connects to a
265
+ Unix domain stream socket instead of a TCP address.
266
+ """
267
+
268
+ def __init__(self, path, timeout=DEFAULT_TIMEOUT):
269
+ super(UnixHTTPConnection, self).__init__("localhost", timeout=timeout)
270
+ self._unix_path = path
271
+
272
+ def connect(self):
273
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
274
+ try:
275
+ sock.settimeout(self.timeout)
276
+ sock.connect(self._unix_path)
277
+ except Exception:
278
+ sock.close()
279
+ raise
280
+ self.sock = sock
281
+
282
+
283
+ def _check_redirect(url, status, response_headers):
284
+ """Return the URL to redirect to, or None for no redirection."""
285
+ if status not in (301, 302, 303, 307, 308):
286
+ return None
287
+ location = response_headers.get("Location")
288
+ if not location:
289
+ return None
290
+ parsed_location = urllib.parse.urlparse(location)
291
+ if parsed_location.scheme:
292
+ # absolute URL
293
+ return location
294
+
295
+ old_url = urllib.parse.urlparse(url)
296
+ if location.startswith("/"):
297
+ # absolute path on old hostname
298
+ return urllib.parse.urlunparse(
299
+ (
300
+ old_url.scheme,
301
+ old_url.netloc,
302
+ parsed_location.path,
303
+ parsed_location.params,
304
+ parsed_location.query,
305
+ parsed_location.fragment,
306
+ )
307
+ )
308
+
309
+ # relative path on old hostname
310
+ old_dir, _old_file = os.path.split(old_url.path)
311
+ new_path = os.path.join(old_dir, location)
312
+ return urllib.parse.urlunparse(
313
+ (
314
+ old_url.scheme,
315
+ old_url.netloc,
316
+ new_path,
317
+ parsed_location.params,
318
+ parsed_location.query,
319
+ parsed_location.fragment,
320
+ )
321
+ )
322
+
323
+
324
+ def _prepare_outgoing_headers(headers):
325
+ if headers is None:
326
+ headers = HTTPMessage()
327
+ elif not isinstance(headers, HTTPMessage):
328
+ new_headers = HTTPMessage()
329
+ if hasattr(headers, "items"):
330
+ iterator = headers.items()
331
+ else:
332
+ iterator = iter(headers)
333
+ for k, v in iterator:
334
+ new_headers[k] = v
335
+ headers = new_headers
336
+ _setdefault_header(headers, "User-Agent", DEFAULT_UA)
337
+ return headers
338
+
339
+
340
+ # XXX join multi-headers together so that get(), __getitem__(),
341
+ # etc. behave intuitively, then stuff them back in an HTTPMessage.
342
+ def _prepare_incoming_headers(headers):
343
+ headers_dict = {}
344
+ for k, v in headers.items():
345
+ headers_dict.setdefault(k, []).append(v)
346
+ result = HTTPMessage()
347
+ # note that iterating over headers_dict preserves the original
348
+ # insertion order in all versions since Python 3.6:
349
+ for k, vlist in headers_dict.items():
350
+ result[k] = ",".join(vlist)
351
+ return result
352
+
353
+
354
+ def _setdefault_header(headers, name, value):
355
+ if name not in headers:
356
+ headers[name] = value
357
+
358
+
359
+ def _prepare_body(body, form, json, headers):
360
+ if body is not None:
361
+ if not isinstance(body, bytes):
362
+ raise TypeError("body must be bytes or None", type(body))
363
+ return body
364
+
365
+ if json is not None:
366
+ _setdefault_header(headers, "Content-Type", _JSON_CONTENTTYPE)
367
+ import json as jsonlib
368
+
369
+ return jsonlib.dumps(json).encode("utf-8")
370
+
371
+ if form is not None:
372
+ _setdefault_header(headers, "Content-Type", _FORM_CONTENTTYPE)
373
+ return urllib.parse.urlencode(form, doseq=True)
374
+
375
+ return None
376
+
377
+
378
+ def _prepare_params(params):
379
+ if params is None:
380
+ return ""
381
+ return urllib.parse.urlencode(params, doseq=True)
382
+
383
+
384
+ def _prepare_request(
385
+ method,
386
+ url,
387
+ *,
388
+ enc_params="",
389
+ timeout=DEFAULT_TIMEOUT,
390
+ source_address=None,
391
+ unix_socket=None,
392
+ verify=True,
393
+ ssl_context=None,
394
+ ):
395
+ """Parses the URL, returns the path and the right HTTPConnection subclass."""
396
+ parsed_url = urllib.parse.urlparse(url)
397
+
398
+ is_unix = unix_socket is not None
399
+ scheme = parsed_url.scheme.lower()
400
+ if scheme.endswith("+unix"):
401
+ scheme = scheme[:-5]
402
+ is_unix = True
403
+ if scheme == "https":
404
+ raise ValueError("https+unix is not implemented")
405
+
406
+ if scheme not in ("http", "https"):
407
+ raise ValueError("unrecognized scheme", scheme)
408
+
409
+ is_https = scheme == "https"
410
+ host = parsed_url.hostname
411
+ port = 443 if is_https else 80
412
+ if parsed_url.port:
413
+ port = parsed_url.port
414
+
415
+ if is_unix and unix_socket is None:
416
+ unix_socket = urllib.parse.unquote(parsed_url.netloc)
417
+
418
+ path = parsed_url.path
419
+ if parsed_url.query:
420
+ if enc_params:
421
+ path = f"{path}?{parsed_url.query}&{enc_params}"
422
+ else:
423
+ path = f"{path}?{parsed_url.query}"
424
+ else:
425
+ if enc_params:
426
+ path = f"{path}?{enc_params}"
427
+ else:
428
+ pass # just parsed_url.path in this case
429
+
430
+ if isinstance(source_address, str):
431
+ source_address = (source_address, 0)
432
+
433
+ if is_unix:
434
+ conn = UnixHTTPConnection(unix_socket, timeout=timeout)
435
+ elif is_https:
436
+ if ssl_context is None:
437
+ ssl_context = ssl.create_default_context()
438
+ if not verify:
439
+ ssl_context.check_hostname = False
440
+ ssl_context.verify_mode = ssl.CERT_NONE
441
+ conn = HTTPSConnection(host, port, source_address=source_address, timeout=timeout, context=ssl_context)
442
+ else:
443
+ conn = HTTPConnection(host, port, source_address=source_address, timeout=timeout)
444
+
445
+ munged_url = urllib.parse.urlunparse(
446
+ (parsed_url.scheme, parsed_url.netloc, path, parsed_url.params, "", parsed_url.fragment)
447
+ )
448
+ return munged_url, conn, path