"""
A requests-based HTTP client for interacting with containers that have
forwarded ports.
"""
import time
import hyperlink
import requests
def _path_segments(url, path_str):
# Absolute path
if path_str.startswith('/'):
return path_str[1:].split('/')
# Relative path
return url.child(*path_str.split('/')).path
[docs]class ContainerHttpClient:
"""
HTTP client for a specific container.
In most cases, these should be obtained from
:meth:`.ContainerDefinition.http_client` instead of being instantiated
directly.
"""
URL_DEFAULTS = {'scheme': 'http'}
def __init__(self, host, port, url_defaults=None, session=None):
"""
:param host:
The address for the host to connect to.
:param port:
The port for the host to connect to.
:param dict url_defaults:
Parameters to default to in the generated URLs, see
`~hyperlink.URL`.
:param session:
A Requests' Session object (or something like it).
"""
if session is None:
session = requests.Session()
self._session = session
_url_defaults = self.URL_DEFAULTS.copy()
if url_defaults is not None:
_url_defaults.update(url_defaults)
self._base_url = hyperlink.URL(
host=host, port=int(port), **_url_defaults)
def __enter__(self):
return self
[docs] def close(self):
"""
Closes the underlying Session object.
"""
self._session.close()
def __exit__(self, *args):
self.close()
[docs] @classmethod
def for_container(cls, container, container_port=None):
"""
:param container:
The container to make requests against.
:param container_port:
The container port to make requests against. If ``None``, the first
container port is used.
:returns:
A ContainerClient object configured to make requests to the
container.
"""
if container_port is not None:
host, port = container.get_host_port(container_port)
else:
host, port = container.get_first_host_port()
return cls(host, port)
def _url(self, path, kwargs):
kwargs = kwargs if kwargs is not None else {}
if path is not None:
kwargs['path'] = _path_segments(self._base_url, path)
return self._base_url.replace(**kwargs).to_text()
[docs] def request(self, method, path=None, url_kwargs=None, **kwargs):
"""
Make a request against a container.
:param method:
The HTTP method to use.
:param list path:
The HTTP path (either absolute or relative).
:param dict url_kwargs:
Parameters to override in the generated URL. See `~hyperlink.URL`.
:param kwargs:
Any other parameters to pass to Requests.
"""
return self._session.request(
method, self._url(path, url_kwargs), **kwargs)
[docs] def get(self, path=None, url_kwargs=None, **kwargs):
"""
Sends a GET request.
:param path:
The HTTP path (either absolute or relative).
:param url_kwargs:
Parameters to override in the generated URL. See `~hyperlink.URL`.
:param **kwargs:
Optional arguments that ``request`` takes.
:return: response object
"""
return self._session.get(self._url(path, url_kwargs), **kwargs)
[docs] def options(self, path=None, url_kwargs=None, **kwargs):
"""
Sends an OPTIONS request.
:param path:
The HTTP path (either absolute or relative).
:param url_kwargs:
Parameters to override in the generated URL. See `~hyperlink.URL`.
:param **kwargs:
Optional arguments that ``request`` takes.
:return: response object
"""
return self._session.options(self._url(path, url_kwargs), **kwargs)
[docs] def head(self, path=None, url_kwargs=None, **kwargs):
"""
Sends a HEAD request.
:param path:
The HTTP path (either absolute or relative).
:param url_kwargs:
Parameters to override in the generated URL. See `~hyperlink.URL`.
:param **kwargs:
Optional arguments that ``request`` takes.
:return: response object
"""
return self._session.head(self._url(path, url_kwargs), **kwargs)
[docs] def post(self, path=None, url_kwargs=None, **kwargs):
"""
Sends a POST request.
:param path:
The HTTP path (either absolute or relative).
:param url_kwargs:
Parameters to override in the generated URL. See `~hyperlink.URL`.
:param **kwargs:
Optional arguments that ``request`` takes.
:return: response object
"""
return self._session.post(self._url(path, url_kwargs), **kwargs)
[docs] def put(self, path=None, url_kwargs=None, **kwargs):
"""
Sends a PUT request.
:param path:
The HTTP path (either absolute or relative).
:param url_kwargs:
Parameters to override in the generated URL. See `~hyperlink.URL`.
:param **kwargs:
Optional arguments that ``request`` takes.
:return: response object
"""
return self._session.put(self._url(path, url_kwargs), **kwargs)
[docs] def patch(self, path=None, url_kwargs=None, **kwargs):
"""
Sends a PUT request.
:param path:
The HTTP path (either absolute or relative).
:param url_kwargs:
Parameters to override in the generated URL. See `~hyperlink.URL`.
:param **kwargs:
Optional arguments that ``request`` takes.
:return: response object
"""
return self._session.patch(self._url(path, url_kwargs), **kwargs)
[docs] def delete(self, path=None, url_kwargs=None, **kwargs):
"""
Sends a PUT request.
:param path:
The HTTP path (either absolute or relative).
:param url_kwargs:
Parameters to override in the generated URL. See `~hyperlink.URL`.
:param **kwargs:
Optional arguments that ``request`` takes.
:return: response object
"""
return self._session.delete(self._url(path, url_kwargs), **kwargs)
[docs]def wait_for_response(client, timeout, path='/', expected_status_code=None):
"""
Try make a GET request with an HTTP client against a certain path and
return once any response has been received, ignoring any errors.
:param ContainerHttpClient client:
The HTTP client to use to connect to the container.
:param timeout:
Timeout value in seconds.
:param path:
HTTP path to request.
:param int expected_status_code:
If set, wait until a response with this status code is received. If not
set, the status code will not be checked.
:raises TimeoutError:
If a request fails to be made within the timeout period.
"""
# We want time.monotonic on Pythons that have it, otherwise time.time will
# have to do.
get_time = getattr(time, 'monotonic', time.time)
deadline = get_time() + timeout
while True:
try:
# Don't care what the response is, as long as we get one
time_left = deadline - get_time()
response = client.get(
path, timeout=max(time_left, 0.001), allow_redirects=False)
if (expected_status_code is None
or response.status_code == expected_status_code):
return
except requests.exceptions.Timeout:
# Requests timed out, our time must be up
break
except Exception:
# Ignore other exceptions
pass
if get_time() >= deadline:
break
time.sleep(0.1)
raise TimeoutError('Timeout waiting for HTTP response.')