pywhales 0.2.1__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.
- pywhales/__init__.py +48 -0
- pywhales/client_config.py +325 -0
- pywhales/command_line_entrypoint.py +8 -0
- pywhales/components/__init__.py +0 -0
- pywhales/components/_swarm_inspect.py +78 -0
- pywhales/components/buildx/__init__.py +0 -0
- pywhales/components/buildx/cli_wrapper.py +1123 -0
- pywhales/components/buildx/imagetools/__init__.py +0 -0
- pywhales/components/buildx/imagetools/cli_wrapper.py +117 -0
- pywhales/components/buildx/imagetools/models.py +40 -0
- pywhales/components/buildx/models.py +27 -0
- pywhales/components/compose/__init__.py +0 -0
- pywhales/components/compose/cli_wrapper.py +1534 -0
- pywhales/components/compose/models.py +147 -0
- pywhales/components/config/__init__.py +0 -0
- pywhales/components/config/cli_wrapper.py +186 -0
- pywhales/components/config/models.py +30 -0
- pywhales/components/container/__init__.py +0 -0
- pywhales/components/container/cli_wrapper.py +2533 -0
- pywhales/components/container/models.py +312 -0
- pywhales/components/container/top.py +85 -0
- pywhales/components/context/__init__.py +0 -0
- pywhales/components/context/cli_wrapper.py +249 -0
- pywhales/components/context/models.py +27 -0
- pywhales/components/image/__init__.py +0 -0
- pywhales/components/image/cli_wrapper.py +1058 -0
- pywhales/components/image/models.py +47 -0
- pywhales/components/image/streams.py +149 -0
- pywhales/components/manifest/__init__.py +0 -0
- pywhales/components/manifest/cli_wrapper.py +180 -0
- pywhales/components/manifest/models.py +9 -0
- pywhales/components/network/__init__.py +0 -0
- pywhales/components/network/cli_wrapper.py +365 -0
- pywhales/components/network/models.py +36 -0
- pywhales/components/node/__init__.py +0 -0
- pywhales/components/node/cli_wrapper.py +274 -0
- pywhales/components/node/models.py +91 -0
- pywhales/components/plugin/__init__.py +0 -0
- pywhales/components/plugin/cli_wrapper.py +308 -0
- pywhales/components/plugin/models.py +39 -0
- pywhales/components/secret/__init__.py +0 -0
- pywhales/components/secret/cli_wrapper.py +176 -0
- pywhales/components/secret/models.py +19 -0
- pywhales/components/service/__init__.py +0 -0
- pywhales/components/service/cli_wrapper.py +617 -0
- pywhales/components/service/models.py +93 -0
- pywhales/components/stack/__init__.py +0 -0
- pywhales/components/stack/cli_wrapper.py +170 -0
- pywhales/components/swarm/__init__.py +0 -0
- pywhales/components/swarm/cli_wrapper.py +229 -0
- pywhales/components/system/__init__.py +0 -0
- pywhales/components/system/cli_wrapper.py +192 -0
- pywhales/components/system/models.py +214 -0
- pywhales/components/task/__init__.py +0 -0
- pywhales/components/task/cli_wrapper.py +117 -0
- pywhales/components/task/models.py +158 -0
- pywhales/components/top.py +172 -0
- pywhales/components/trust/__init__.py +0 -0
- pywhales/components/trust/cli_wrapper.py +22 -0
- pywhales/components/volume/__init__.py +0 -0
- pywhales/components/volume/cli_wrapper.py +393 -0
- pywhales/components/volume/models.py +16 -0
- pywhales/docker_client.py +391 -0
- pywhales/exceptions.py +62 -0
- pywhales/py.typed +1 -0
- pywhales/test_utils.py +25 -0
- pywhales/utils.py +541 -0
- pywhales-0.2.1.dist-info/METADATA +206 -0
- pywhales-0.2.1.dist-info/RECORD +73 -0
- pywhales-0.2.1.dist-info/WHEEL +5 -0
- pywhales-0.2.1.dist-info/entry_points.txt +2 -0
- pywhales-0.2.1.dist-info/licenses/LICENSE +21 -0
- pywhales-0.2.1.dist-info/top_level.txt +1 -0
pywhales/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from .client_config import ClientNotFoundError
|
|
2
|
+
from .components.buildx.cli_wrapper import Builder
|
|
3
|
+
from .components.config.cli_wrapper import Config
|
|
4
|
+
from .components.container.cli_wrapper import Container, ContainerStats
|
|
5
|
+
from .components.context.cli_wrapper import (
|
|
6
|
+
Context,
|
|
7
|
+
DockerContextConfig,
|
|
8
|
+
KubernetesContextConfig,
|
|
9
|
+
)
|
|
10
|
+
from .components.image.cli_wrapper import Image
|
|
11
|
+
from .components.network.cli_wrapper import Network
|
|
12
|
+
from .components.node.cli_wrapper import Node
|
|
13
|
+
from .components.plugin.cli_wrapper import Plugin
|
|
14
|
+
from .components.secret.cli_wrapper import Secret
|
|
15
|
+
from .components.service.cli_wrapper import Service
|
|
16
|
+
from .components.stack.cli_wrapper import Stack
|
|
17
|
+
from .components.system.cli_wrapper import SystemInfo
|
|
18
|
+
from .components.task.cli_wrapper import Task
|
|
19
|
+
from .components.volume.cli_wrapper import Volume
|
|
20
|
+
from .docker_client import DockerClient, Version
|
|
21
|
+
from .exceptions import DockerException
|
|
22
|
+
|
|
23
|
+
# alias
|
|
24
|
+
docker = DockerClient(client_type="docker")
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"Builder",
|
|
28
|
+
"ClientNotFoundError",
|
|
29
|
+
"Config",
|
|
30
|
+
"Container",
|
|
31
|
+
"ContainerStats",
|
|
32
|
+
"Context",
|
|
33
|
+
"DockerClient",
|
|
34
|
+
"DockerContextConfig",
|
|
35
|
+
"DockerException",
|
|
36
|
+
"Image",
|
|
37
|
+
"KubernetesContextConfig",
|
|
38
|
+
"Network",
|
|
39
|
+
"Node",
|
|
40
|
+
"Plugin",
|
|
41
|
+
"Secret",
|
|
42
|
+
"Service",
|
|
43
|
+
"Stack",
|
|
44
|
+
"SystemInfo",
|
|
45
|
+
"Task",
|
|
46
|
+
"Version",
|
|
47
|
+
"Volume",
|
|
48
|
+
]
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import tempfile
|
|
5
|
+
import warnings
|
|
6
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Literal, override
|
|
11
|
+
|
|
12
|
+
import pydantic
|
|
13
|
+
|
|
14
|
+
from . import utils
|
|
15
|
+
from .utils import ValidPath, run, to_list
|
|
16
|
+
|
|
17
|
+
CACHE_VALIDITY_PERIOD: float = 0.01
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ParsingError(Exception):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ClientNotFoundError(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Command(list[object]):
|
|
29
|
+
def add_simple_arg(self, name: str, value: object) -> None:
|
|
30
|
+
if value is not None:
|
|
31
|
+
self.extend([name, value])
|
|
32
|
+
|
|
33
|
+
def add_flag(self, name: str, value: bool) -> None:
|
|
34
|
+
if value:
|
|
35
|
+
self.append(name)
|
|
36
|
+
|
|
37
|
+
def add_args_iterable(self, arg_name: str, values: Iterable[object]) -> None:
|
|
38
|
+
for value in values:
|
|
39
|
+
self.extend([arg_name, value])
|
|
40
|
+
|
|
41
|
+
def add_args_iterable_or_single(
|
|
42
|
+
self, arg_name: str, iterable_or_single: object
|
|
43
|
+
) -> None:
|
|
44
|
+
for value in to_list(iterable_or_single):
|
|
45
|
+
self.extend([arg_name, value])
|
|
46
|
+
|
|
47
|
+
def add_args_mapping(
|
|
48
|
+
self, arg_name: str, mapping: Mapping[str, object], *, separator: str = "="
|
|
49
|
+
) -> None:
|
|
50
|
+
self.add_args_iterable(
|
|
51
|
+
arg_name, utils.format_mapping_for_cli(mapping, separator)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
@override
|
|
55
|
+
def __add__[ElementT](self, other: list[ElementT]) -> Command:
|
|
56
|
+
return Command(super().__add__(other))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class ClientConfig:
|
|
61
|
+
config: ValidPath | None = None
|
|
62
|
+
context: str | None = None
|
|
63
|
+
debug: bool | None = None
|
|
64
|
+
host: str | None = None
|
|
65
|
+
log_level: str | None = None
|
|
66
|
+
tls: bool | None = None
|
|
67
|
+
tlscacert: ValidPath | None = None
|
|
68
|
+
tlscert: ValidPath | None = None
|
|
69
|
+
tlskey: ValidPath | None = None
|
|
70
|
+
tlsverify: bool | None = None
|
|
71
|
+
client_binary_path: ValidPath | None = None
|
|
72
|
+
compose_files: list[ValidPath] = field(default_factory=list)
|
|
73
|
+
compose_profiles: list[str] = field(default_factory=list)
|
|
74
|
+
compose_env_file: ValidPath | None = None
|
|
75
|
+
compose_env_files: Sequence[ValidPath] = field(default_factory=list)
|
|
76
|
+
compose_project_name: str | None = None
|
|
77
|
+
compose_project_directory: ValidPath | None = None
|
|
78
|
+
compose_compatibility: bool | None = None
|
|
79
|
+
client_call: list[str] = field(default_factory=lambda: ["docker"])
|
|
80
|
+
client_type: Literal["docker", "nerdctl", "unknown"] = "unknown"
|
|
81
|
+
_client_call_with_path: list[Path | str] | None = None
|
|
82
|
+
compose_dry_run: bool = False
|
|
83
|
+
compose_parallel: int | None = None
|
|
84
|
+
compose_progress: Literal["auto", "tty", "plain", "json", "quiet"] | None = None
|
|
85
|
+
|
|
86
|
+
def get_client_call_with_path(self) -> list[Path | str]:
|
|
87
|
+
if self._client_call_with_path is None:
|
|
88
|
+
self._client_call_with_path = [
|
|
89
|
+
Path(self._get_docker_path()),
|
|
90
|
+
*self.client_call[1:],
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
return self._client_call_with_path
|
|
94
|
+
|
|
95
|
+
def _get_docker_path(self) -> str:
|
|
96
|
+
which_result = shutil.which(self.client_call[0])
|
|
97
|
+
if which_result is None:
|
|
98
|
+
raise ClientNotFoundError(
|
|
99
|
+
f"The binary '{self.client_call[0]}' could not be found on your PATH. "
|
|
100
|
+
"Please ensure that your PATH has the directory of the binary you're looking for. "
|
|
101
|
+
"You can use `print(os.environ['PATH'])` to verify what directories are in your PATH."
|
|
102
|
+
)
|
|
103
|
+
return which_result
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def docker_cmd(self) -> Command:
|
|
107
|
+
result = Command(self.get_client_call_with_path())
|
|
108
|
+
|
|
109
|
+
if self.config is not None:
|
|
110
|
+
result += ["--config", self.config]
|
|
111
|
+
|
|
112
|
+
if self.context is not None:
|
|
113
|
+
result += ["--context", self.context]
|
|
114
|
+
|
|
115
|
+
if self.debug is True:
|
|
116
|
+
result.append("--debug")
|
|
117
|
+
|
|
118
|
+
if self.host is not None:
|
|
119
|
+
result += ["--host", self.host]
|
|
120
|
+
|
|
121
|
+
if self.log_level is not None:
|
|
122
|
+
result += ["--log-level", self.log_level]
|
|
123
|
+
|
|
124
|
+
if self.tls is True:
|
|
125
|
+
result.append("--tls")
|
|
126
|
+
|
|
127
|
+
if self.tlscacert is not None:
|
|
128
|
+
result += ["--tlscacert", self.tlscacert]
|
|
129
|
+
|
|
130
|
+
if self.tlscert is not None:
|
|
131
|
+
result += ["--tlscert", self.tlscert]
|
|
132
|
+
|
|
133
|
+
if self.tlskey is not None:
|
|
134
|
+
result += ["--tlskey", self.tlskey]
|
|
135
|
+
|
|
136
|
+
if self.tlsverify is True:
|
|
137
|
+
result.append("--tlsverify")
|
|
138
|
+
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def docker_compose_cmd(self) -> Command:
|
|
143
|
+
return self.get_compose_command()
|
|
144
|
+
|
|
145
|
+
def get_compose_command(self, *, progress: str | None = None) -> Command:
|
|
146
|
+
"""Build Compose globals, optionally overriding progress for one command."""
|
|
147
|
+
base_cmd = Command([*self.docker_cmd, "compose"])
|
|
148
|
+
base_cmd.add_args_iterable_or_single("--file", self.compose_files)
|
|
149
|
+
base_cmd.add_args_iterable_or_single("--profile", self.compose_profiles)
|
|
150
|
+
if self.compose_env_files:
|
|
151
|
+
if self.compose_env_file is not None:
|
|
152
|
+
warnings.warn(
|
|
153
|
+
"You can't set both `compose_env_file` and `compose_env_files`. Files used in `compose_env_files` will be used.",
|
|
154
|
+
stacklevel=2,
|
|
155
|
+
)
|
|
156
|
+
base_cmd.add_args_iterable("--env-file", self.compose_env_files)
|
|
157
|
+
elif self.compose_env_file is not None:
|
|
158
|
+
warnings.warn(
|
|
159
|
+
"`compose_env_file` is deprecated. Use `compose_env_files` instead.",
|
|
160
|
+
stacklevel=2,
|
|
161
|
+
)
|
|
162
|
+
base_cmd.add_simple_arg("--env-file", self.compose_env_file)
|
|
163
|
+
|
|
164
|
+
base_cmd.add_simple_arg("--project-name", self.compose_project_name)
|
|
165
|
+
base_cmd.add_simple_arg("--project-directory", self.compose_project_directory)
|
|
166
|
+
base_cmd.add_flag("--compatibility", self.compose_compatibility is True)
|
|
167
|
+
base_cmd.add_flag("--dry-run", self.compose_dry_run)
|
|
168
|
+
base_cmd.add_simple_arg("--parallel", self.compose_parallel)
|
|
169
|
+
base_cmd.add_simple_arg(
|
|
170
|
+
"--progress", self.compose_progress if progress is None else progress
|
|
171
|
+
)
|
|
172
|
+
return base_cmd
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class DockerCLICaller:
|
|
176
|
+
def __init__(self, client_config: ClientConfig) -> None:
|
|
177
|
+
self.client_config = client_config
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def docker_cmd(self) -> Command:
|
|
181
|
+
return self.client_config.docker_cmd
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def docker_compose_cmd(self) -> Command:
|
|
185
|
+
return self.client_config.docker_compose_cmd
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class ReloadableObject[InspectResultT, ReferenceT: str | None = str](DockerCLICaller):
|
|
189
|
+
def __init__(
|
|
190
|
+
self,
|
|
191
|
+
client_config: ClientConfig,
|
|
192
|
+
id_in_inspect: str,
|
|
193
|
+
reference_or_id: ReferenceT,
|
|
194
|
+
is_immutable_id: bool = False,
|
|
195
|
+
) -> None:
|
|
196
|
+
super().__init__(client_config)
|
|
197
|
+
self._last_refreshed_time = datetime.min
|
|
198
|
+
self._inspect_result: InspectResultT | None = None
|
|
199
|
+
self._id_in_inspect = id_in_inspect
|
|
200
|
+
if is_immutable_id:
|
|
201
|
+
self._immutable_id: ReferenceT = reference_or_id
|
|
202
|
+
else:
|
|
203
|
+
inspect_result = self._fetch_and_parse_inspect_result(reference_or_id)
|
|
204
|
+
self._set_inspect_result(inspect_result)
|
|
205
|
+
self._immutable_id = getattr(inspect_result, id_in_inspect)
|
|
206
|
+
|
|
207
|
+
@override
|
|
208
|
+
def __eq__(self, other: object) -> bool:
|
|
209
|
+
if not isinstance(other, ReloadableObject):
|
|
210
|
+
return False
|
|
211
|
+
return (
|
|
212
|
+
self._get_immutable_id() == other._get_immutable_id()
|
|
213
|
+
and self.client_config == other.client_config
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
@override
|
|
217
|
+
def __str__(self) -> str:
|
|
218
|
+
reference = self._get_immutable_id()
|
|
219
|
+
if reference is None:
|
|
220
|
+
raise TypeError("__str__ returned non-string (type NoneType)")
|
|
221
|
+
return reference
|
|
222
|
+
|
|
223
|
+
def _needs_reload(self) -> bool:
|
|
224
|
+
return (datetime.now() - self._last_refreshed_time) >= timedelta(
|
|
225
|
+
seconds=CACHE_VALIDITY_PERIOD
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def reload(self) -> None:
|
|
229
|
+
self._reload()
|
|
230
|
+
|
|
231
|
+
def _reload(self) -> InspectResultT:
|
|
232
|
+
inspect_result = self._fetch_and_parse_inspect_result(self._immutable_id)
|
|
233
|
+
self._set_inspect_result(inspect_result)
|
|
234
|
+
return inspect_result
|
|
235
|
+
|
|
236
|
+
def _fetch_and_parse_inspect_result(self, reference: ReferenceT) -> InspectResultT:
|
|
237
|
+
raise NotImplementedError
|
|
238
|
+
|
|
239
|
+
def _get_inspect_result(self) -> InspectResultT:
|
|
240
|
+
if self._inspect_result is None or self._needs_reload():
|
|
241
|
+
return self._reload()
|
|
242
|
+
return self._inspect_result
|
|
243
|
+
|
|
244
|
+
def _set_inspect_result(self, inspect_result: InspectResultT) -> None:
|
|
245
|
+
self._inspect_result = inspect_result
|
|
246
|
+
self._last_refreshed_time = datetime.now()
|
|
247
|
+
|
|
248
|
+
def _get_immutable_id(self) -> ReferenceT:
|
|
249
|
+
return self._immutable_id
|
|
250
|
+
|
|
251
|
+
@override
|
|
252
|
+
def __hash__(self) -> int:
|
|
253
|
+
# maybe we can do better.
|
|
254
|
+
return hash(self._get_immutable_id())
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class ReloadableObjectFromJson[InspectResultT, ReferenceT: str | None = str](
|
|
258
|
+
ReloadableObject[InspectResultT, ReferenceT]
|
|
259
|
+
):
|
|
260
|
+
def _fetch_inspect_result_json(self, reference: ReferenceT) -> dict[str, Any]:
|
|
261
|
+
raise NotImplementedError
|
|
262
|
+
|
|
263
|
+
def _parse_json_object(self, json_object: dict[str, Any]) -> InspectResultT:
|
|
264
|
+
raise NotImplementedError
|
|
265
|
+
|
|
266
|
+
@override
|
|
267
|
+
def _fetch_and_parse_inspect_result(self, reference: ReferenceT) -> InspectResultT:
|
|
268
|
+
json_object = self._fetch_inspect_result_json(reference)
|
|
269
|
+
return self._parse_inspect_result(json_object)
|
|
270
|
+
|
|
271
|
+
def _parse_inspect_results(
|
|
272
|
+
self, json_str: str, references: Sequence[str]
|
|
273
|
+
) -> list[InspectResultT]:
|
|
274
|
+
try:
|
|
275
|
+
json_objects = pydantic.TypeAdapter(
|
|
276
|
+
list[dict[str, pydantic.JsonValue]]
|
|
277
|
+
).validate_json(json_str)
|
|
278
|
+
except pydantic.ValidationError as err:
|
|
279
|
+
raise ParsingError("Invalid batch inspect JSON response") from err
|
|
280
|
+
if len(json_objects) != len(references):
|
|
281
|
+
raise ParsingError(
|
|
282
|
+
f"Expected {len(references)} inspect results, received {len(json_objects)}"
|
|
283
|
+
)
|
|
284
|
+
results = [self._parse_inspect_result(item) for item in json_objects]
|
|
285
|
+
resolved: dict[str, str] = {}
|
|
286
|
+
for reference, result in zip(references, results, strict=True):
|
|
287
|
+
identity = getattr(result, self._id_in_inspect)
|
|
288
|
+
if not isinstance(identity, str) or not identity:
|
|
289
|
+
raise ParsingError(f"Missing inspect identity for {reference!r}")
|
|
290
|
+
if reference in resolved and resolved[reference] != identity:
|
|
291
|
+
raise ParsingError(f"Inconsistent inspect identity for {reference!r}")
|
|
292
|
+
resolved[reference] = identity
|
|
293
|
+
return results
|
|
294
|
+
|
|
295
|
+
def _parse_inspect_result(self, json_object: dict[str, Any]) -> InspectResultT:
|
|
296
|
+
try:
|
|
297
|
+
return self._parse_json_object(json_object)
|
|
298
|
+
except pydantic.ValidationError as err:
|
|
299
|
+
fd, json_response_file = tempfile.mkstemp(suffix=".json", text=True)
|
|
300
|
+
with os.fdopen(fd, "w") as f:
|
|
301
|
+
json.dump(json_object, f, indent=2)
|
|
302
|
+
|
|
303
|
+
raise ParsingError(
|
|
304
|
+
f"There was an error parsing the json response from the Docker daemon. \n"
|
|
305
|
+
f"This is a bug with pywhales itself. Please head to \n"
|
|
306
|
+
f"https://github.com/MaraClaw/PyWhales/issues \n"
|
|
307
|
+
f"and open an issue. You should copy this error message and \n"
|
|
308
|
+
f"the json response from the Docker daemon. The json response was put \n"
|
|
309
|
+
f"in {json_response_file} because it's a bit too big to be printed \n"
|
|
310
|
+
f"on the screen. Make sure that there are no sensitive data in the \n"
|
|
311
|
+
f"json file before copying it in the github issue."
|
|
312
|
+
) from err
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def bulk_reload[InspectResultT, ReferenceT: str | None](
|
|
316
|
+
docker_objects: Sequence[ReloadableObjectFromJson[InspectResultT, ReferenceT]],
|
|
317
|
+
) -> None:
|
|
318
|
+
assert len({x.client_config for x in docker_objects}) == 1
|
|
319
|
+
all_ids = [x._get_immutable_id() for x in docker_objects]
|
|
320
|
+
full_cmd = [*docker_objects[0].docker_cmd, "inspect", *all_ids]
|
|
321
|
+
json_str = run(full_cmd)
|
|
322
|
+
for json_obj, docker_object in zip(
|
|
323
|
+
json.loads(json_str), docker_objects, strict=False
|
|
324
|
+
):
|
|
325
|
+
docker_object._set_inspect_result(docker_object._parse_json_object(json_obj))
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
def main() -> None:
|
|
2
|
+
print(
|
|
3
|
+
"The PyWhales command line interface does not perform any action. "
|
|
4
|
+
"If you were using it to download the docker CLI, "
|
|
5
|
+
"you should instead do it manually. "
|
|
6
|
+
"Take a look at https://github.com/MaraClaw/PyWhales/blob/HEAD/docs/template/docker_client.md#the-docker-cli "
|
|
7
|
+
"which has instructions to download the Docker client."
|
|
8
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from typing import assert_never
|
|
4
|
+
|
|
5
|
+
from pywhales.client_config import ParsingError, ReloadableObjectFromJson
|
|
6
|
+
from pywhales.components.config.models import ConfigInspectResult
|
|
7
|
+
from pywhales.components.node.models import NodeInspectResult
|
|
8
|
+
from pywhales.components.secret.models import SecretInspectResult
|
|
9
|
+
from pywhales.components.service.models import ServiceInspectResult
|
|
10
|
+
|
|
11
|
+
type SwarmInspectResult = (
|
|
12
|
+
ConfigInspectResult | NodeInspectResult | SecretInspectResult | ServiceInspectResult
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _inspect_name(result: SwarmInspectResult) -> str | None:
|
|
17
|
+
match result:
|
|
18
|
+
case NodeInspectResult(description=description):
|
|
19
|
+
return description.hostname if description is not None else None
|
|
20
|
+
case (
|
|
21
|
+
ConfigInspectResult(spec=spec)
|
|
22
|
+
| SecretInspectResult(spec=spec)
|
|
23
|
+
| ServiceInspectResult(spec=spec)
|
|
24
|
+
):
|
|
25
|
+
return spec.name if spec is not None else None
|
|
26
|
+
case unreachable:
|
|
27
|
+
assert_never(unreachable)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_swarm_inspect_results[InspectResultT: SwarmInspectResult](
|
|
31
|
+
parser: ReloadableObjectFromJson[InspectResultT],
|
|
32
|
+
json_str: str,
|
|
33
|
+
references: Sequence[str],
|
|
34
|
+
) -> list[tuple[str, InspectResultT]]:
|
|
35
|
+
"""Validate response slots, then resolve references independently of their order.
|
|
36
|
+
|
|
37
|
+
Unique slot labels keep the shared parser's positional duplicate check from
|
|
38
|
+
associating repeated request references with unrelated reordered responses.
|
|
39
|
+
Identity consistency and multiplicity are checked after reference resolution.
|
|
40
|
+
"""
|
|
41
|
+
results = parser._parse_inspect_results(
|
|
42
|
+
json_str, [str(index) for index in range(len(references))]
|
|
43
|
+
)
|
|
44
|
+
snapshots: dict[str, InspectResultT] = {}
|
|
45
|
+
identities: list[str] = []
|
|
46
|
+
for result in results:
|
|
47
|
+
identity = result.id
|
|
48
|
+
if identity is None or identity == "":
|
|
49
|
+
raise ParsingError("Missing Swarm inspect identity")
|
|
50
|
+
if identity in snapshots and snapshots[identity] != result:
|
|
51
|
+
raise ParsingError(f"Inconsistent inspect snapshot for {identity!r}")
|
|
52
|
+
snapshots[identity] = result
|
|
53
|
+
identities.append(identity)
|
|
54
|
+
|
|
55
|
+
resolved: list[tuple[str, InspectResultT]] = []
|
|
56
|
+
for reference in references:
|
|
57
|
+
if reference in snapshots:
|
|
58
|
+
resolved.append((reference, snapshots[reference]))
|
|
59
|
+
continue
|
|
60
|
+
matches = [
|
|
61
|
+
(identity, result)
|
|
62
|
+
for identity, result in snapshots.items()
|
|
63
|
+
if reference and _inspect_name(result) == reference
|
|
64
|
+
]
|
|
65
|
+
if not matches:
|
|
66
|
+
matches = [
|
|
67
|
+
(identity, result)
|
|
68
|
+
for identity, result in snapshots.items()
|
|
69
|
+
if reference and identity.startswith(reference)
|
|
70
|
+
]
|
|
71
|
+
if len(matches) != 1:
|
|
72
|
+
raise ParsingError(
|
|
73
|
+
f"Expected one inspect identity for {reference!r}, received {len(matches)}"
|
|
74
|
+
)
|
|
75
|
+
resolved.append(matches[0])
|
|
76
|
+
if Counter(identity for identity, _ in resolved) != Counter(identities):
|
|
77
|
+
raise ParsingError("Inspect identities do not match requested multiplicities")
|
|
78
|
+
return resolved
|
|
File without changes
|