mirror of
https://github.com/ansible-collections/community.docker.git
synced 2026-07-29 11:55:04 +00:00
Add typing information, 1/2 (#1176)
* Re-enable typing and improve config. * Make mypy pass. * Improve settings. * First batch of types. * Add more type hints. * Fixes. * Format. * Fix split_port() without returning to previous type chaos. * Continue with type hints (and ignores).
This commit is contained in:
@@ -13,8 +13,9 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
from functools import partial
|
||||
import typing as t
|
||||
from urllib.parse import quote
|
||||
|
||||
from .. import auth
|
||||
@@ -47,16 +48,21 @@ from ..transport.sshconn import PARAMIKO_IMPORT_ERROR, SSHHTTPAdapter
|
||||
from ..transport.ssladapter import SSLHTTPAdapter
|
||||
from ..transport.unixconn import UnixHTTPAdapter
|
||||
from ..utils import config, json_stream, utils
|
||||
from ..utils.decorators import update_headers
|
||||
from ..utils.decorators import minimum_version, update_headers
|
||||
from ..utils.proxy import ProxyConfig
|
||||
from ..utils.socket import consume_socket_output, demux_adaptor, frames_iter
|
||||
from .daemon import DaemonApiMixin
|
||||
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from requests import Response
|
||||
|
||||
from ..._socket_helper import SocketLike
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class APIClient(_Session, DaemonApiMixin):
|
||||
class APIClient(_Session):
|
||||
"""
|
||||
A low-level client for the Docker Engine API.
|
||||
|
||||
@@ -105,16 +111,16 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url=None,
|
||||
version=None,
|
||||
timeout=DEFAULT_TIMEOUT_SECONDS,
|
||||
tls=False,
|
||||
user_agent=DEFAULT_USER_AGENT,
|
||||
num_pools=None,
|
||||
credstore_env=None,
|
||||
use_ssh_client=False,
|
||||
max_pool_size=DEFAULT_MAX_POOL_SIZE,
|
||||
):
|
||||
base_url: str | None = None,
|
||||
version: str | None = None,
|
||||
timeout: int | float = DEFAULT_TIMEOUT_SECONDS,
|
||||
tls: bool | TLSConfig = False,
|
||||
user_agent: str = DEFAULT_USER_AGENT,
|
||||
num_pools: int | None = None,
|
||||
credstore_env: dict[str, str] | None = None,
|
||||
use_ssh_client: bool = False,
|
||||
max_pool_size: int = DEFAULT_MAX_POOL_SIZE,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
fail_on_missing_imports()
|
||||
@@ -124,7 +130,6 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
"If using TLS, the base_url argument must be provided."
|
||||
)
|
||||
|
||||
self.base_url = base_url
|
||||
self.timeout = timeout
|
||||
self.headers["User-Agent"] = user_agent
|
||||
|
||||
@@ -145,6 +150,7 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
self.credstore_env = credstore_env
|
||||
|
||||
base_url = utils.parse_host(base_url, IS_WINDOWS_PLATFORM, tls=bool(tls))
|
||||
self.base_url = base_url
|
||||
# SSH has a different default for num_pools to all other adapters
|
||||
num_pools = (
|
||||
num_pools or DEFAULT_NUM_POOLS_SSH
|
||||
@@ -152,6 +158,9 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
else DEFAULT_NUM_POOLS
|
||||
)
|
||||
|
||||
self._custom_adapter: (
|
||||
UnixHTTPAdapter | NpipeHTTPAdapter | SSHHTTPAdapter | SSLHTTPAdapter | None
|
||||
) = None
|
||||
if base_url.startswith("http+unix://"):
|
||||
self._custom_adapter = UnixHTTPAdapter(
|
||||
base_url,
|
||||
@@ -223,7 +232,7 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
f"API versions below {MINIMUM_DOCKER_API_VERSION} are no longer supported by this library."
|
||||
)
|
||||
|
||||
def _retrieve_server_version(self):
|
||||
def _retrieve_server_version(self) -> str:
|
||||
try:
|
||||
version_result = self.version(api_version=False)
|
||||
except Exception as e:
|
||||
@@ -242,54 +251,87 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
f"Error while fetching server API version: {e}. Response seems to be broken."
|
||||
) from e
|
||||
|
||||
def _set_request_timeout(self, kwargs):
|
||||
def _set_request_timeout(self, kwargs: dict[str, t.Any]) -> dict[str, t.Any]:
|
||||
"""Prepare the kwargs for an HTTP request by inserting the timeout
|
||||
parameter, if not already present."""
|
||||
kwargs.setdefault("timeout", self.timeout)
|
||||
return kwargs
|
||||
|
||||
@update_headers
|
||||
def _post(self, url, **kwargs):
|
||||
def _post(self, url: str, **kwargs):
|
||||
return self.post(url, **self._set_request_timeout(kwargs))
|
||||
|
||||
@update_headers
|
||||
def _get(self, url, **kwargs):
|
||||
def _get(self, url: str, **kwargs):
|
||||
return self.get(url, **self._set_request_timeout(kwargs))
|
||||
|
||||
@update_headers
|
||||
def _head(self, url, **kwargs):
|
||||
def _head(self, url: str, **kwargs):
|
||||
return self.head(url, **self._set_request_timeout(kwargs))
|
||||
|
||||
@update_headers
|
||||
def _put(self, url, **kwargs):
|
||||
def _put(self, url: str, **kwargs):
|
||||
return self.put(url, **self._set_request_timeout(kwargs))
|
||||
|
||||
@update_headers
|
||||
def _delete(self, url, **kwargs):
|
||||
def _delete(self, url: str, **kwargs):
|
||||
return self.delete(url, **self._set_request_timeout(kwargs))
|
||||
|
||||
def _url(self, pathfmt, *args, **kwargs):
|
||||
def _url(self, pathfmt: str, *args: str, versioned_api: bool = True) -> str:
|
||||
for arg in args:
|
||||
if not isinstance(arg, str):
|
||||
raise ValueError(
|
||||
f"Expected a string but found {arg} ({type(arg)}) instead"
|
||||
)
|
||||
|
||||
quote_f = partial(quote, safe="/:")
|
||||
args = map(quote_f, args)
|
||||
q_args = [quote(arg, safe="/:") for arg in args]
|
||||
|
||||
if kwargs.get("versioned_api", True):
|
||||
return f"{self.base_url}/v{self._version}{pathfmt.format(*args)}"
|
||||
return f"{self.base_url}{pathfmt.format(*args)}"
|
||||
if versioned_api:
|
||||
return f"{self.base_url}/v{self._version}{pathfmt.format(*q_args)}"
|
||||
return f"{self.base_url}{pathfmt.format(*q_args)}"
|
||||
|
||||
def _raise_for_status(self, response):
|
||||
def _raise_for_status(self, response: Response) -> None:
|
||||
"""Raises stored :class:`APIError`, if one occurred."""
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except _HTTPError as e:
|
||||
create_api_error_from_http_exception(e)
|
||||
|
||||
def _result(self, response, get_json=False, get_binary=False):
|
||||
@t.overload
|
||||
def _result(
|
||||
self,
|
||||
response: Response,
|
||||
*,
|
||||
get_json: t.Literal[False] = False,
|
||||
get_binary: t.Literal[False] = False,
|
||||
) -> str: ...
|
||||
|
||||
@t.overload
|
||||
def _result(
|
||||
self,
|
||||
response: Response,
|
||||
*,
|
||||
get_json: t.Literal[True],
|
||||
get_binary: t.Literal[False] = False,
|
||||
) -> t.Any: ...
|
||||
|
||||
@t.overload
|
||||
def _result(
|
||||
self,
|
||||
response: Response,
|
||||
*,
|
||||
get_json: t.Literal[False] = False,
|
||||
get_binary: t.Literal[True],
|
||||
) -> bytes: ...
|
||||
|
||||
@t.overload
|
||||
def _result(
|
||||
self, response: Response, *, get_json: bool = False, get_binary: bool = False
|
||||
) -> t.Any | str | bytes: ...
|
||||
|
||||
def _result(
|
||||
self, response: Response, *, get_json: bool = False, get_binary: bool = False
|
||||
) -> t.Any | str | bytes:
|
||||
if get_json and get_binary:
|
||||
raise AssertionError("json and binary must not be both True")
|
||||
self._raise_for_status(response)
|
||||
@@ -300,10 +342,12 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
return response.content
|
||||
return response.text
|
||||
|
||||
def _post_json(self, url, data, **kwargs):
|
||||
def _post_json(
|
||||
self, url: str, data: dict[str, str | None] | t.Any, **kwargs
|
||||
) -> Response:
|
||||
# Go <1.1 cannot unserialize null to a string
|
||||
# so we do this disgusting thing here.
|
||||
data2 = {}
|
||||
data2: dict[str, t.Any] = {}
|
||||
if data is not None and isinstance(data, dict):
|
||||
for k, v in data.items():
|
||||
if v is not None:
|
||||
@@ -316,19 +360,19 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
kwargs["headers"]["Content-Type"] = "application/json"
|
||||
return self._post(url, data=json.dumps(data2), **kwargs)
|
||||
|
||||
def _attach_params(self, override=None):
|
||||
def _attach_params(self, override: dict[str, int] | None = None) -> dict[str, int]:
|
||||
return override or {"stdout": 1, "stderr": 1, "stream": 1}
|
||||
|
||||
def _get_raw_response_socket(self, response):
|
||||
def _get_raw_response_socket(self, response: Response) -> SocketLike:
|
||||
self._raise_for_status(response)
|
||||
if self.base_url == "http+docker://localnpipe":
|
||||
sock = response.raw._fp.fp.raw.sock
|
||||
sock = response.raw._fp.fp.raw.sock # type: ignore[union-attr]
|
||||
elif self.base_url.startswith("http+docker://ssh"):
|
||||
sock = response.raw._fp.fp.channel
|
||||
sock = response.raw._fp.fp.channel # type: ignore[union-attr]
|
||||
else:
|
||||
sock = response.raw._fp.fp.raw
|
||||
sock = response.raw._fp.fp.raw # type: ignore[union-attr]
|
||||
if self.base_url.startswith("https://"):
|
||||
sock = sock._sock
|
||||
sock = sock._sock # type: ignore[union-attr]
|
||||
try:
|
||||
# Keep a reference to the response to stop it being garbage
|
||||
# collected. If the response is garbage collected, it will
|
||||
@@ -341,12 +385,26 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
|
||||
return sock
|
||||
|
||||
def _stream_helper(self, response, decode=False):
|
||||
@t.overload
|
||||
def _stream_helper(
|
||||
self, response: Response, *, decode: t.Literal[False] = False
|
||||
) -> t.Generator[bytes]: ...
|
||||
|
||||
@t.overload
|
||||
def _stream_helper(
|
||||
self, response: Response, *, decode: t.Literal[True]
|
||||
) -> t.Generator[t.Any]: ...
|
||||
|
||||
def _stream_helper(
|
||||
self, response: Response, *, decode: bool = False
|
||||
) -> t.Generator[t.Any]:
|
||||
"""Generator for data coming from a chunked-encoded HTTP response."""
|
||||
|
||||
if response.raw._fp.chunked:
|
||||
if response.raw._fp.chunked: # type: ignore[union-attr]
|
||||
if decode:
|
||||
yield from json_stream.json_stream(self._stream_helper(response, False))
|
||||
yield from json_stream.json_stream(
|
||||
self._stream_helper(response, decode=False)
|
||||
)
|
||||
else:
|
||||
reader = response.raw
|
||||
while not reader.closed:
|
||||
@@ -354,15 +412,15 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
data = reader.read(1)
|
||||
if not data:
|
||||
break
|
||||
if reader._fp.chunk_left:
|
||||
data += reader.read(reader._fp.chunk_left)
|
||||
if reader._fp.chunk_left: # type: ignore[union-attr]
|
||||
data += reader.read(reader._fp.chunk_left) # type: ignore[union-attr]
|
||||
yield data
|
||||
else:
|
||||
# Response is not chunked, meaning we probably
|
||||
# encountered an error immediately
|
||||
yield self._result(response, get_json=decode)
|
||||
|
||||
def _multiplexed_buffer_helper(self, response):
|
||||
def _multiplexed_buffer_helper(self, response: Response) -> t.Generator[bytes]:
|
||||
"""A generator of multiplexed data blocks read from a buffered
|
||||
response."""
|
||||
buf = self._result(response, get_binary=True)
|
||||
@@ -378,7 +436,9 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
walker = end
|
||||
yield buf[start:end]
|
||||
|
||||
def _multiplexed_response_stream_helper(self, response):
|
||||
def _multiplexed_response_stream_helper(
|
||||
self, response: Response
|
||||
) -> t.Generator[bytes]:
|
||||
"""A generator of multiplexed data blocks coming from a response
|
||||
stream."""
|
||||
|
||||
@@ -399,7 +459,19 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
break
|
||||
yield data
|
||||
|
||||
def _stream_raw_result(self, response, chunk_size=1, decode=True):
|
||||
@t.overload
|
||||
def _stream_raw_result(
|
||||
self, response: Response, *, chunk_size: int = 1, decode: t.Literal[True] = True
|
||||
) -> t.Generator[str]: ...
|
||||
|
||||
@t.overload
|
||||
def _stream_raw_result(
|
||||
self, response: Response, *, chunk_size: int = 1, decode: t.Literal[False]
|
||||
) -> t.Generator[bytes]: ...
|
||||
|
||||
def _stream_raw_result(
|
||||
self, response: Response, *, chunk_size: int = 1, decode: bool = True
|
||||
) -> t.Generator[str | bytes]:
|
||||
"""Stream result for TTY-enabled container and raw binary data"""
|
||||
self._raise_for_status(response)
|
||||
|
||||
@@ -410,14 +482,81 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
|
||||
yield from response.iter_content(chunk_size, decode)
|
||||
|
||||
def _read_from_socket(self, response, stream, tty=True, demux=False):
|
||||
@t.overload
|
||||
def _read_from_socket(
|
||||
self,
|
||||
response: Response,
|
||||
*,
|
||||
stream: t.Literal[True],
|
||||
tty: bool = True,
|
||||
demux: t.Literal[False] = False,
|
||||
) -> t.Generator[bytes]: ...
|
||||
|
||||
@t.overload
|
||||
def _read_from_socket(
|
||||
self,
|
||||
response: Response,
|
||||
*,
|
||||
stream: t.Literal[True],
|
||||
tty: t.Literal[True] = True,
|
||||
demux: t.Literal[True],
|
||||
) -> t.Generator[tuple[bytes, None]]: ...
|
||||
|
||||
@t.overload
|
||||
def _read_from_socket(
|
||||
self,
|
||||
response: Response,
|
||||
*,
|
||||
stream: t.Literal[True],
|
||||
tty: t.Literal[False],
|
||||
demux: t.Literal[True],
|
||||
) -> t.Generator[tuple[bytes, None] | tuple[None, bytes]]: ...
|
||||
|
||||
@t.overload
|
||||
def _read_from_socket(
|
||||
self,
|
||||
response: Response,
|
||||
*,
|
||||
stream: t.Literal[False],
|
||||
tty: bool = True,
|
||||
demux: t.Literal[False] = False,
|
||||
) -> bytes: ...
|
||||
|
||||
@t.overload
|
||||
def _read_from_socket(
|
||||
self,
|
||||
response: Response,
|
||||
*,
|
||||
stream: t.Literal[False],
|
||||
tty: t.Literal[True] = True,
|
||||
demux: t.Literal[True],
|
||||
) -> tuple[bytes, None]: ...
|
||||
|
||||
@t.overload
|
||||
def _read_from_socket(
|
||||
self,
|
||||
response: Response,
|
||||
*,
|
||||
stream: t.Literal[False],
|
||||
tty: t.Literal[False],
|
||||
demux: t.Literal[True],
|
||||
) -> tuple[bytes, bytes]: ...
|
||||
|
||||
@t.overload
|
||||
def _read_from_socket(
|
||||
self, response: Response, *, stream: bool, tty: bool = True, demux: bool = False
|
||||
) -> t.Any: ...
|
||||
|
||||
def _read_from_socket(
|
||||
self, response: Response, *, stream: bool, tty: bool = True, demux: bool = False
|
||||
) -> t.Any:
|
||||
"""Consume all data from the socket, close the response and return the
|
||||
data. If stream=True, then a generator is returned instead and the
|
||||
caller is responsible for closing the response.
|
||||
"""
|
||||
socket = self._get_raw_response_socket(response)
|
||||
|
||||
gen = frames_iter(socket, tty)
|
||||
gen: t.Generator = frames_iter(socket, tty)
|
||||
|
||||
if demux:
|
||||
# The generator will output tuples (stdout, stderr)
|
||||
@@ -434,7 +573,7 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
finally:
|
||||
response.close()
|
||||
|
||||
def _disable_socket_timeout(self, socket):
|
||||
def _disable_socket_timeout(self, socket: SocketLike) -> None:
|
||||
"""Depending on the combination of python version and whether we are
|
||||
connecting over http or https, we might need to access _sock, which
|
||||
may or may not exist; or we may need to just settimeout on socket
|
||||
@@ -451,18 +590,38 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
if not hasattr(s, "settimeout"):
|
||||
continue
|
||||
|
||||
timeout = -1
|
||||
timeout: int | float | None = -1
|
||||
|
||||
if hasattr(s, "gettimeout"):
|
||||
timeout = s.gettimeout()
|
||||
timeout = s.gettimeout() # type: ignore[union-attr]
|
||||
|
||||
# Do not change the timeout if it is already disabled.
|
||||
if timeout is None or timeout == 0.0:
|
||||
continue
|
||||
|
||||
s.settimeout(None)
|
||||
s.settimeout(None) # type: ignore[union-attr]
|
||||
|
||||
def _get_result_tty(self, stream, res, is_tty):
|
||||
@t.overload
|
||||
def _get_result_tty(
|
||||
self, stream: t.Literal[True], res: Response, is_tty: t.Literal[True]
|
||||
) -> t.Generator[str]: ...
|
||||
|
||||
@t.overload
|
||||
def _get_result_tty(
|
||||
self, stream: t.Literal[True], res: Response, is_tty: t.Literal[False]
|
||||
) -> t.Generator[bytes]: ...
|
||||
|
||||
@t.overload
|
||||
def _get_result_tty(
|
||||
self, stream: t.Literal[False], res: Response, is_tty: t.Literal[True]
|
||||
) -> bytes: ...
|
||||
|
||||
@t.overload
|
||||
def _get_result_tty(
|
||||
self, stream: t.Literal[False], res: Response, is_tty: t.Literal[False]
|
||||
) -> bytes: ...
|
||||
|
||||
def _get_result_tty(self, stream: bool, res: Response, is_tty: bool) -> t.Any:
|
||||
# We should also use raw streaming (without keep-alive)
|
||||
# if we are dealing with a tty-enabled container.
|
||||
if is_tty:
|
||||
@@ -478,11 +637,11 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
return self._multiplexed_response_stream_helper(res)
|
||||
return sep.join(list(self._multiplexed_buffer_helper(res)))
|
||||
|
||||
def _unmount(self, *args):
|
||||
def _unmount(self, *args) -> None:
|
||||
for proto in args:
|
||||
self.adapters.pop(proto)
|
||||
|
||||
def get_adapter(self, url):
|
||||
def get_adapter(self, url: str):
|
||||
try:
|
||||
return super().get_adapter(url)
|
||||
except _InvalidSchema as e:
|
||||
@@ -491,10 +650,10 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
raise e
|
||||
|
||||
@property
|
||||
def api_version(self):
|
||||
def api_version(self) -> str:
|
||||
return self._version
|
||||
|
||||
def reload_config(self, dockercfg_path=None):
|
||||
def reload_config(self, dockercfg_path: str | None = None) -> None:
|
||||
"""
|
||||
Force a reload of the auth configuration
|
||||
|
||||
@@ -510,7 +669,7 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
dockercfg_path, credstore_env=self.credstore_env
|
||||
)
|
||||
|
||||
def _set_auth_headers(self, headers):
|
||||
def _set_auth_headers(self, headers: dict[str, str | bytes]) -> None:
|
||||
log.debug("Looking for auth config")
|
||||
|
||||
# If we do not have any auth data so far, try reloading the config
|
||||
@@ -537,57 +696,62 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
else:
|
||||
log.debug("No auth config found")
|
||||
|
||||
def get_binary(self, pathfmt, *args, **kwargs):
|
||||
def get_binary(self, pathfmt: str, *args: str, **kwargs) -> bytes:
|
||||
return self._result(
|
||||
self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs),
|
||||
get_binary=True,
|
||||
)
|
||||
|
||||
def get_json(self, pathfmt, *args, **kwargs):
|
||||
def get_json(self, pathfmt: str, *args: str, **kwargs) -> t.Any:
|
||||
return self._result(
|
||||
self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs),
|
||||
get_json=True,
|
||||
)
|
||||
|
||||
def get_text(self, pathfmt, *args, **kwargs):
|
||||
def get_text(self, pathfmt: str, *args: str, **kwargs) -> str:
|
||||
return self._result(
|
||||
self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs)
|
||||
)
|
||||
|
||||
def get_raw_stream(self, pathfmt, *args, **kwargs):
|
||||
chunk_size = kwargs.pop("chunk_size", DEFAULT_DATA_CHUNK_SIZE)
|
||||
def get_raw_stream(
|
||||
self,
|
||||
pathfmt: str,
|
||||
*args: str,
|
||||
chunk_size: int = DEFAULT_DATA_CHUNK_SIZE,
|
||||
**kwargs,
|
||||
) -> t.Generator[bytes]:
|
||||
res = self._get(
|
||||
self._url(pathfmt, *args, versioned_api=True), stream=True, **kwargs
|
||||
)
|
||||
self._raise_for_status(res)
|
||||
return self._stream_raw_result(res, chunk_size, False)
|
||||
return self._stream_raw_result(res, chunk_size=chunk_size, decode=False)
|
||||
|
||||
def delete_call(self, pathfmt, *args, **kwargs):
|
||||
def delete_call(self, pathfmt: str, *args: str, **kwargs) -> None:
|
||||
self._raise_for_status(
|
||||
self._delete(self._url(pathfmt, *args, versioned_api=True), **kwargs)
|
||||
)
|
||||
|
||||
def delete_json(self, pathfmt, *args, **kwargs):
|
||||
def delete_json(self, pathfmt: str, *args: str, **kwargs) -> t.Any:
|
||||
return self._result(
|
||||
self._delete(self._url(pathfmt, *args, versioned_api=True), **kwargs),
|
||||
get_json=True,
|
||||
)
|
||||
|
||||
def post_call(self, pathfmt, *args, **kwargs):
|
||||
def post_call(self, pathfmt: str, *args: str, **kwargs) -> None:
|
||||
self._raise_for_status(
|
||||
self._post(self._url(pathfmt, *args, versioned_api=True), **kwargs)
|
||||
)
|
||||
|
||||
def post_json(self, pathfmt, *args, **kwargs):
|
||||
data = kwargs.pop("data", None)
|
||||
def post_json(self, pathfmt: str, *args: str, data: t.Any = None, **kwargs) -> None:
|
||||
self._raise_for_status(
|
||||
self._post_json(
|
||||
self._url(pathfmt, *args, versioned_api=True), data, **kwargs
|
||||
)
|
||||
)
|
||||
|
||||
def post_json_to_binary(self, pathfmt, *args, **kwargs):
|
||||
data = kwargs.pop("data", None)
|
||||
def post_json_to_binary(
|
||||
self, pathfmt: str, *args: str, data: t.Any = None, **kwargs
|
||||
) -> bytes:
|
||||
return self._result(
|
||||
self._post_json(
|
||||
self._url(pathfmt, *args, versioned_api=True), data, **kwargs
|
||||
@@ -595,8 +759,9 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
get_binary=True,
|
||||
)
|
||||
|
||||
def post_json_to_json(self, pathfmt, *args, **kwargs):
|
||||
data = kwargs.pop("data", None)
|
||||
def post_json_to_json(
|
||||
self, pathfmt: str, *args: str, data: t.Any = None, **kwargs
|
||||
) -> t.Any:
|
||||
return self._result(
|
||||
self._post_json(
|
||||
self._url(pathfmt, *args, versioned_api=True), data, **kwargs
|
||||
@@ -604,17 +769,24 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
get_json=True,
|
||||
)
|
||||
|
||||
def post_json_to_text(self, pathfmt, *args, **kwargs):
|
||||
data = kwargs.pop("data", None)
|
||||
def post_json_to_text(
|
||||
self, pathfmt: str, *args: str, data: t.Any = None, **kwargs
|
||||
) -> str:
|
||||
return self._result(
|
||||
self._post_json(
|
||||
self._url(pathfmt, *args, versioned_api=True), data, **kwargs
|
||||
),
|
||||
)
|
||||
|
||||
def post_json_to_stream_socket(self, pathfmt, *args, **kwargs):
|
||||
data = kwargs.pop("data", None)
|
||||
headers = (kwargs.pop("headers", None) or {}).copy()
|
||||
def post_json_to_stream_socket(
|
||||
self,
|
||||
pathfmt: str,
|
||||
*args: str,
|
||||
data: t.Any = None,
|
||||
headers: dict[str, str] | None = None,
|
||||
**kwargs,
|
||||
) -> SocketLike:
|
||||
headers = headers.copy() if headers else {}
|
||||
headers.update(
|
||||
{
|
||||
"Connection": "Upgrade",
|
||||
@@ -631,18 +803,102 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
)
|
||||
)
|
||||
|
||||
def post_json_to_stream(self, pathfmt, *args, **kwargs):
|
||||
data = kwargs.pop("data", None)
|
||||
headers = (kwargs.pop("headers", None) or {}).copy()
|
||||
@t.overload
|
||||
def post_json_to_stream(
|
||||
self,
|
||||
pathfmt: str,
|
||||
*args: str,
|
||||
data: t.Any = None,
|
||||
headers: dict[str, str] | None = None,
|
||||
stream: t.Literal[True],
|
||||
tty: bool = True,
|
||||
demux: t.Literal[False] = False,
|
||||
**kwargs,
|
||||
) -> t.Generator[bytes]: ...
|
||||
|
||||
@t.overload
|
||||
def post_json_to_stream(
|
||||
self,
|
||||
pathfmt: str,
|
||||
*args: str,
|
||||
data: t.Any = None,
|
||||
headers: dict[str, str] | None = None,
|
||||
stream: t.Literal[True],
|
||||
tty: t.Literal[True] = True,
|
||||
demux: t.Literal[True],
|
||||
**kwargs,
|
||||
) -> t.Generator[tuple[bytes, None]]: ...
|
||||
|
||||
@t.overload
|
||||
def post_json_to_stream(
|
||||
self,
|
||||
pathfmt: str,
|
||||
*args: str,
|
||||
data: t.Any = None,
|
||||
headers: dict[str, str] | None = None,
|
||||
stream: t.Literal[True],
|
||||
tty: t.Literal[False],
|
||||
demux: t.Literal[True],
|
||||
**kwargs,
|
||||
) -> t.Generator[tuple[bytes, None] | tuple[None, bytes]]: ...
|
||||
|
||||
@t.overload
|
||||
def post_json_to_stream(
|
||||
self,
|
||||
pathfmt: str,
|
||||
*args: str,
|
||||
data: t.Any = None,
|
||||
headers: dict[str, str] | None = None,
|
||||
stream: t.Literal[False],
|
||||
tty: bool = True,
|
||||
demux: t.Literal[False] = False,
|
||||
**kwargs,
|
||||
) -> bytes: ...
|
||||
|
||||
@t.overload
|
||||
def post_json_to_stream(
|
||||
self,
|
||||
pathfmt: str,
|
||||
*args: str,
|
||||
data: t.Any = None,
|
||||
headers: dict[str, str] | None = None,
|
||||
stream: t.Literal[False],
|
||||
tty: t.Literal[True] = True,
|
||||
demux: t.Literal[True],
|
||||
**kwargs,
|
||||
) -> tuple[bytes, None]: ...
|
||||
|
||||
@t.overload
|
||||
def post_json_to_stream(
|
||||
self,
|
||||
pathfmt: str,
|
||||
*args: str,
|
||||
data: t.Any = None,
|
||||
headers: dict[str, str] | None = None,
|
||||
stream: t.Literal[False],
|
||||
tty: t.Literal[False],
|
||||
demux: t.Literal[True],
|
||||
**kwargs,
|
||||
) -> tuple[bytes, bytes]: ...
|
||||
|
||||
def post_json_to_stream(
|
||||
self,
|
||||
pathfmt: str,
|
||||
*args: str,
|
||||
data: t.Any = None,
|
||||
headers: dict[str, str] | None = None,
|
||||
stream: bool = False,
|
||||
demux: bool = False,
|
||||
tty: bool = False,
|
||||
**kwargs,
|
||||
) -> t.Any:
|
||||
headers = headers.copy() if headers else {}
|
||||
headers.update(
|
||||
{
|
||||
"Connection": "Upgrade",
|
||||
"Upgrade": "tcp",
|
||||
}
|
||||
)
|
||||
stream = kwargs.pop("stream", False)
|
||||
demux = kwargs.pop("demux", False)
|
||||
tty = kwargs.pop("tty", False)
|
||||
return self._read_from_socket(
|
||||
self._post_json(
|
||||
self._url(pathfmt, *args, versioned_api=True),
|
||||
@@ -651,13 +907,133 @@ class APIClient(_Session, DaemonApiMixin):
|
||||
stream=True,
|
||||
**kwargs,
|
||||
),
|
||||
stream,
|
||||
stream=stream,
|
||||
tty=tty,
|
||||
demux=demux,
|
||||
)
|
||||
|
||||
def post_to_json(self, pathfmt, *args, **kwargs):
|
||||
def post_to_json(self, pathfmt: str, *args: str, **kwargs) -> t.Any:
|
||||
return self._result(
|
||||
self._post(self._url(pathfmt, *args, versioned_api=True), **kwargs),
|
||||
get_json=True,
|
||||
)
|
||||
|
||||
@minimum_version("1.25")
|
||||
def df(self) -> dict[str, t.Any]:
|
||||
"""
|
||||
Get data usage information.
|
||||
|
||||
Returns:
|
||||
(dict): A dictionary representing different resource categories
|
||||
and their respective data usage.
|
||||
|
||||
Raises:
|
||||
:py:class:`docker.errors.APIError`
|
||||
If the server returns an error.
|
||||
"""
|
||||
url = self._url("/system/df")
|
||||
return self._result(self._get(url), get_json=True)
|
||||
|
||||
def info(self) -> dict[str, t.Any]:
|
||||
"""
|
||||
Display system-wide information. Identical to the ``docker info``
|
||||
command.
|
||||
|
||||
Returns:
|
||||
(dict): The info as a dict
|
||||
|
||||
Raises:
|
||||
:py:class:`docker.errors.APIError`
|
||||
If the server returns an error.
|
||||
"""
|
||||
return self._result(self._get(self._url("/info")), get_json=True)
|
||||
|
||||
def login(
|
||||
self,
|
||||
username: str,
|
||||
password: str | None = None,
|
||||
email: str | None = None,
|
||||
registry: str | None = None,
|
||||
reauth: bool = False,
|
||||
dockercfg_path: str | None = None,
|
||||
) -> dict[str, t.Any]:
|
||||
"""
|
||||
Authenticate with a registry. Similar to the ``docker login`` command.
|
||||
|
||||
Args:
|
||||
username (str): The registry username
|
||||
password (str): The plaintext password
|
||||
email (str): The email for the registry account
|
||||
registry (str): URL to the registry. E.g.
|
||||
``https://index.docker.io/v1/``
|
||||
reauth (bool): Whether or not to refresh existing authentication on
|
||||
the Docker server.
|
||||
dockercfg_path (str): Use a custom path for the Docker config file
|
||||
(default ``$HOME/.docker/config.json`` if present,
|
||||
otherwise ``$HOME/.dockercfg``)
|
||||
|
||||
Returns:
|
||||
(dict): The response from the login request
|
||||
|
||||
Raises:
|
||||
:py:class:`docker.errors.APIError`
|
||||
If the server returns an error.
|
||||
"""
|
||||
|
||||
# If we do not have any auth data so far, try reloading the config file
|
||||
# one more time in case anything showed up in there.
|
||||
# If dockercfg_path is passed check to see if the config file exists,
|
||||
# if so load that config.
|
||||
if dockercfg_path and os.path.exists(dockercfg_path):
|
||||
self._auth_configs = auth.load_config(
|
||||
dockercfg_path, credstore_env=self.credstore_env
|
||||
)
|
||||
elif not self._auth_configs or self._auth_configs.is_empty:
|
||||
self._auth_configs = auth.load_config(credstore_env=self.credstore_env)
|
||||
|
||||
authcfg = self._auth_configs.resolve_authconfig(registry)
|
||||
# If we found an existing auth config for this registry and username
|
||||
# combination, we can return it immediately unless reauth is requested.
|
||||
if authcfg and authcfg.get("username", None) == username and not reauth:
|
||||
return authcfg
|
||||
|
||||
req_data = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
"email": email,
|
||||
"serveraddress": registry,
|
||||
}
|
||||
|
||||
response = self._post_json(self._url("/auth"), data=req_data)
|
||||
if response.status_code == 200:
|
||||
self._auth_configs.add_auth(registry or auth.INDEX_NAME, req_data)
|
||||
return self._result(response, get_json=True)
|
||||
|
||||
def ping(self) -> bool:
|
||||
"""
|
||||
Checks the server is responsive. An exception will be raised if it
|
||||
is not responding.
|
||||
|
||||
Returns:
|
||||
(bool) The response from the server.
|
||||
|
||||
Raises:
|
||||
:py:class:`docker.errors.APIError`
|
||||
If the server returns an error.
|
||||
"""
|
||||
return self._result(self._get(self._url("/_ping"))) == "OK"
|
||||
|
||||
def version(self, api_version: bool = True) -> dict[str, t.Any]:
|
||||
"""
|
||||
Returns version information from the server. Similar to the ``docker
|
||||
version`` command.
|
||||
|
||||
Returns:
|
||||
(dict): The server version information
|
||||
|
||||
Raises:
|
||||
:py:class:`docker.errors.APIError`
|
||||
If the server returns an error.
|
||||
"""
|
||||
url = self._url("/version", versioned_api=api_version)
|
||||
return self._result(self._get(url), get_json=True)
|
||||
|
||||
Reference in New Issue
Block a user