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:
Felix Fontein
2025-10-23 07:05:42 +02:00
committed by GitHub
parent 24f35644e3
commit 3350283bcc
92 changed files with 4366 additions and 2272 deletions
+462 -86
View File
@@ -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)