mirror of https://github.com/ansible/ansible.git
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
122 lines
3.8 KiB
Python
122 lines
3.8 KiB
Python
"""A simple HTTP client."""
|
|
from __future__ import annotations
|
|
|
|
import http.client
|
|
import json
|
|
import time
|
|
import typing as t
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
from .util import (
|
|
ApplicationError,
|
|
display,
|
|
)
|
|
|
|
from .util_common import (
|
|
CommonConfig,
|
|
)
|
|
|
|
|
|
class HttpClient:
|
|
"""Make HTTP requests."""
|
|
|
|
def __init__(self, args: CommonConfig, always: bool = False) -> None:
|
|
self.args = args
|
|
self.always = always
|
|
|
|
def get(self, url: str) -> HttpResponse:
|
|
"""Perform an HTTP GET and return the response."""
|
|
return self.request('GET', url)
|
|
|
|
def delete(self, url: str) -> HttpResponse:
|
|
"""Perform an HTTP DELETE and return the response."""
|
|
return self.request('DELETE', url)
|
|
|
|
def put(self, url: str, data: t.Optional[str] = None, headers: t.Optional[dict[str, str]] = None) -> HttpResponse:
|
|
"""Perform an HTTP PUT and return the response."""
|
|
return self.request('PUT', url, data, headers)
|
|
|
|
def request(self, method: str, url: str, data: t.Optional[str] = None, headers: t.Optional[dict[str, str]] = None) -> HttpResponse:
|
|
"""Perform an HTTP request and return the response."""
|
|
if headers is None:
|
|
headers = {}
|
|
|
|
data_bytes = data.encode() if data else None
|
|
|
|
request = urllib.request.Request(method=method, url=url, data=data_bytes, headers=headers)
|
|
response: http.client.HTTPResponse
|
|
|
|
display.info(f'HTTP {method} {url}', verbosity=2)
|
|
|
|
attempts = 0
|
|
max_attempts = 3
|
|
sleep_seconds = 3
|
|
|
|
status_code = 200
|
|
reason = 'OK'
|
|
body_bytes = b''
|
|
|
|
while True:
|
|
attempts += 1
|
|
|
|
start = time.monotonic()
|
|
|
|
if self.args.explain and not self.always:
|
|
break
|
|
|
|
try:
|
|
try:
|
|
with urllib.request.urlopen(request) as response:
|
|
status_code = response.status
|
|
reason = response.reason
|
|
body_bytes = response.read()
|
|
except urllib.error.HTTPError as ex:
|
|
status_code = ex.status
|
|
reason = ex.reason
|
|
body_bytes = ex.read()
|
|
except Exception as ex: # pylint: disable=broad-exception-caught
|
|
if attempts >= max_attempts:
|
|
raise
|
|
|
|
# all currently implemented methods are idempotent, so retries are unconditionally supported
|
|
duration = time.monotonic() - start
|
|
display.warning(f'{type(ex).__module__}.{type(ex).__name__}: {ex} [{duration:.2f} seconds]')
|
|
time.sleep(sleep_seconds)
|
|
|
|
continue
|
|
|
|
break
|
|
|
|
duration = time.monotonic() - start
|
|
display.info(f'HTTP {method} {url} -> HTTP {status_code} ({reason}) [{len(body_bytes)} bytes, {duration:.2f} seconds]', verbosity=3)
|
|
|
|
body = body_bytes.decode()
|
|
|
|
return HttpResponse(method, url, status_code, body)
|
|
|
|
|
|
class HttpResponse:
|
|
"""HTTP response."""
|
|
|
|
def __init__(self, method: str, url: str, status_code: int, response: str) -> None:
|
|
self.method = method
|
|
self.url = url
|
|
self.status_code = status_code
|
|
self.response = response
|
|
|
|
def json(self) -> t.Any:
|
|
"""Return the response parsed as JSON, raising an exception if parsing fails."""
|
|
try:
|
|
return json.loads(self.response)
|
|
except ValueError:
|
|
raise HttpError(self.status_code, 'Cannot parse response to %s %s as JSON:\n%s' % (self.method, self.url, self.response)) from None
|
|
|
|
|
|
class HttpError(ApplicationError):
|
|
"""HTTP response as an error."""
|
|
|
|
def __init__(self, status: int, message: str) -> None:
|
|
super().__init__('%s: %s' % (status, message))
|
|
self.status = status
|