hostctl 0.1.0__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.
Files changed (51) hide show
  1. hostctl/AGENTS.md +292 -0
  2. hostctl/README.md +248 -0
  3. hostctl/__init__.py +194 -0
  4. hostctl/__main__.py +6 -0
  5. hostctl/_async.py +169 -0
  6. hostctl/_cli.py +232 -0
  7. hostctl/executor/__init__.py +82 -0
  8. hostctl/executor/_common.py +234 -0
  9. hostctl/executor/_qga.py +646 -0
  10. hostctl/executor/container.py +171 -0
  11. hostctl/executor/local.py +91 -0
  12. hostctl/executor/psrp.py +145 -0
  13. hostctl/executor/qemu.py +305 -0
  14. hostctl/executor/serial.py +312 -0
  15. hostctl/executor/ssh.py +226 -0
  16. hostctl/executor/winrm.py +255 -0
  17. hostctl/host/__init__.py +93 -0
  18. hostctl/host/_common.py +804 -0
  19. hostctl/host/_local.py +258 -0
  20. hostctl/host/_ssh.py +587 -0
  21. hostctl/host/_winrm.py +1002 -0
  22. hostctl/host/composite_path.py +567 -0
  23. hostctl/host/container.py +606 -0
  24. hostctl/host/container_path.py +664 -0
  25. hostctl/host/qemu.py +1078 -0
  26. hostctl/host/serial.py +401 -0
  27. hostctl/host/system.py +809 -0
  28. hostctl/process/__init__.py +49 -0
  29. hostctl/process/_common.py +113 -0
  30. hostctl/process/container.py +265 -0
  31. hostctl/process/psrp.py +208 -0
  32. hostctl/process/qemu_serial.py +247 -0
  33. hostctl/process/serial.py +257 -0
  34. hostctl/process/ssh.py +165 -0
  35. hostctl/provider/__init__.py +33 -0
  36. hostctl/provider/_common.py +452 -0
  37. hostctl/provider/transports.py +303 -0
  38. hostctl/py.typed +0 -0
  39. hostctl/serial/__init__.py +285 -0
  40. hostctl/shell/__init__.py +123 -0
  41. hostctl/shell/_common.py +616 -0
  42. hostctl/shell/cmd.py +167 -0
  43. hostctl/shell/fish.py +63 -0
  44. hostctl/shell/posix.py +83 -0
  45. hostctl/shell/powershell.py +120 -0
  46. hostctl/sync.py +245 -0
  47. hostctl-0.1.0.dist-info/METADATA +302 -0
  48. hostctl-0.1.0.dist-info/RECORD +51 -0
  49. hostctl-0.1.0.dist-info/WHEEL +4 -0
  50. hostctl-0.1.0.dist-info/entry_points.txt +2 -0
  51. hostctl-0.1.0.dist-info/licenses/LICENSE +21 -0
hostctl/host/_local.py ADDED
@@ -0,0 +1,258 @@
1
+ """Local host implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os as _os
6
+ import platform as _platform
7
+ import subprocess as _subprocess
8
+ import typing as _ty
9
+
10
+ from ..executor import LocalExecutor
11
+ from ..provider import OperationNotStarted, ProviderSelector
12
+ from ..provider.transports import LocalExecutorProvider, LocalPathProvider
13
+ from ._common import (
14
+ CaptureOutput,
15
+ Command,
16
+ Environment,
17
+ FileHandle,
18
+ Host,
19
+ HostConfig,
20
+ HostInfo,
21
+ HostPath,
22
+ Input,
23
+ PathLike,
24
+ starts_direct_command,
25
+ normalize_os_family,
26
+ strict_uri_credentials,
27
+ )
28
+ from ..shell import POSIX_SHELL, POWERSHELL, ShellFlavour
29
+
30
+
31
+ class LocalConfig(HostConfig, schemes=("local",)):
32
+ def __init__(self) -> None:
33
+ super().__init__()
34
+
35
+ @property
36
+ def connection_uri(self) -> str:
37
+ return "local:"
38
+
39
+ @classmethod
40
+ def _from_parsed_uri(cls, parsed, **credentials: object) -> LocalConfig:
41
+ strict_uri_credentials(credentials, ())
42
+ if parsed.netloc or parsed.path or parsed.query:
43
+ raise ValueError("local URI must be exactly 'local:'")
44
+ return cls()
45
+
46
+ def _create_host(self) -> LocalHost:
47
+ return LocalHost(self)
48
+
49
+
50
+ class LocalHost(Host):
51
+ """A host whose commands and paths are local to this process.
52
+
53
+ The public surface is unchanged, but execution and filesystem access are
54
+ assembled from ordered providers (:class:`LocalExecutorProvider` and
55
+ :class:`LocalPathProvider`) rather than a hard-wired executor. A subclass
56
+ may supply its own providers to reuse the local semantics over a different
57
+ access mechanism.
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ config: _ty.Optional[LocalConfig] = None,
63
+ *,
64
+ executor_providers: _ty.Iterable[object] = (),
65
+ path_providers: _ty.Iterable[object] = (),
66
+ ) -> None:
67
+ self.config = config or LocalConfig()
68
+ self._executor_provider_selector = ProviderSelector(
69
+ tuple(executor_providers) or (LocalExecutorProvider(),)
70
+ )
71
+ self._path_provider_selector = ProviderSelector(
72
+ tuple(path_providers) or (LocalPathProvider(),)
73
+ )
74
+
75
+ @property
76
+ def executor_providers(self) -> _ty.Tuple[object, ...]:
77
+ """The ordered command providers backing :meth:`run`."""
78
+ return self._executor_provider_selector.providers
79
+
80
+ @property
81
+ def path_providers(self) -> _ty.Tuple[object, ...]:
82
+ """The ordered filesystem providers backing :meth:`path`."""
83
+ return self._path_provider_selector.providers
84
+
85
+ def _run_selector(self) -> ProviderSelector:
86
+ return self._executor_provider_selector
87
+
88
+ @property
89
+ def capabilities(self) -> _ty.FrozenSet[str]:
90
+ values = set()
91
+ if any(
92
+ self._executor_provider_selector.probe(provider).usable
93
+ for provider in self._executor_provider_selector.providers
94
+ ):
95
+ values.add("run")
96
+ if any(
97
+ self._path_provider_selector.probe(provider).usable
98
+ for provider in self._path_provider_selector.providers
99
+ ):
100
+ values.add("path")
101
+ return frozenset(values)
102
+
103
+ @property
104
+ def shell_flavour(self) -> ShellFlavour:
105
+ if _os.name == "nt":
106
+ return POWERSHELL
107
+ if _os.name == "posix":
108
+ return POSIX_SHELL
109
+ raise NotImplementedError(f"unsupported local OS: {_os.name}")
110
+
111
+ @property
112
+ def executor(self) -> LocalExecutor:
113
+ return self._executor_provider_selector.select().provider.executor
114
+
115
+ def info(self) -> HostInfo:
116
+ return HostInfo(
117
+ hostname=_platform.node() or None,
118
+ os_family=normalize_os_family(_platform.system()),
119
+ os_name=_platform.system() or None,
120
+ os_version=_platform.version() or None,
121
+ architecture=_platform.machine() or None,
122
+ )
123
+
124
+ def path(self, *segments: PathLike, backend: _ty.Optional[str] = None) -> HostPath:
125
+ names = tuple(
126
+ provider.name for provider in self._path_provider_selector.providers
127
+ )
128
+ if backend is not None and backend not in names:
129
+ raise ValueError(
130
+ "local path backend must be "
131
+ + " or ".join(repr(name) for name in names or ("local",))
132
+ )
133
+ if backend is None:
134
+ provider = self._path_provider_selector.select().provider
135
+ else:
136
+ provider = next(
137
+ item
138
+ for item in self._path_provider_selector.providers
139
+ if item.name == backend
140
+ )
141
+ if not provider.probe().usable:
142
+ raise OperationNotStarted(f"path provider {backend!r} is unavailable")
143
+ return provider.path(*(segments or (_os.getcwd(),)))
144
+
145
+ def run(
146
+ self,
147
+ *cmds: Command,
148
+ bufsize: int = -1,
149
+ executable: _ty.Optional[str] = None,
150
+ stdin: _ty.Optional[FileHandle] = None,
151
+ stdout: _ty.Optional[FileHandle] = None,
152
+ stderr: _ty.Optional[FileHandle] = None,
153
+ cwd: _ty.Optional[PathLike] = None,
154
+ env: _ty.Optional[Environment] = None,
155
+ capture_output: CaptureOutput = True,
156
+ check: bool = True,
157
+ encoding: _ty.Optional[str] = None,
158
+ errors: _ty.Optional[str] = None,
159
+ input: Input = None,
160
+ timeout: _ty.Optional[float] = None,
161
+ text: _ty.Optional[bool] = None,
162
+ ) -> _subprocess.CompletedProcess:
163
+ excluded: _ty.List[str] = []
164
+ while True:
165
+ provider = self._executor_provider_selector.select(
166
+ exclude=excluded
167
+ ).provider
168
+ try:
169
+ return self._run_with_provider(
170
+ provider,
171
+ cmds,
172
+ bufsize=bufsize,
173
+ executable=executable,
174
+ stdin=stdin,
175
+ stdout=stdout,
176
+ stderr=stderr,
177
+ cwd=cwd,
178
+ env=env,
179
+ capture_output=capture_output,
180
+ check=check,
181
+ encoding=encoding,
182
+ errors=errors,
183
+ input=input,
184
+ timeout=timeout,
185
+ text=text,
186
+ )
187
+ except OperationNotStarted as exc:
188
+ # Only a proven pre-dispatch refusal may reach another
189
+ # provider; a dispatched command is never replayed.
190
+ self._executor_provider_selector.decline(provider.name, str(exc))
191
+ excluded.append(provider.name)
192
+
193
+ def _run_with_provider(
194
+ self,
195
+ provider,
196
+ cmds: _ty.Sequence[Command],
197
+ *,
198
+ bufsize: int,
199
+ executable: _ty.Optional[str],
200
+ stdin,
201
+ stdout,
202
+ stderr,
203
+ cwd,
204
+ env,
205
+ capture_output,
206
+ check,
207
+ encoding,
208
+ errors,
209
+ input,
210
+ timeout,
211
+ text,
212
+ ) -> _subprocess.CompletedProcess:
213
+ direct = starts_direct_command(cmds)
214
+ if direct is not None:
215
+ command, args = direct
216
+ if executable is not None:
217
+ raise NotImplementedError(
218
+ "executable cannot be combined with a direct command"
219
+ )
220
+ return provider.execute(
221
+ command,
222
+ *args,
223
+ bufsize=bufsize,
224
+ stdin=stdin,
225
+ stdout=stdout,
226
+ stderr=stderr,
227
+ cwd=cwd,
228
+ env=env,
229
+ capture_output=capture_output,
230
+ check=check,
231
+ encoding=encoding,
232
+ errors=errors,
233
+ input=input,
234
+ timeout=timeout,
235
+ text=text,
236
+ )
237
+ script = self.shell_flavour.script(cmds, cwd=None, env=None)
238
+ invocation = self.shell_flavour.invocation(
239
+ script,
240
+ executable=executable,
241
+ )
242
+ return provider.execute(
243
+ invocation[0],
244
+ *invocation[1:],
245
+ bufsize=bufsize,
246
+ stdin=stdin,
247
+ stdout=stdout,
248
+ stderr=stderr,
249
+ cwd=cwd,
250
+ env=env,
251
+ capture_output=capture_output,
252
+ check=check,
253
+ encoding=encoding,
254
+ errors=errors,
255
+ input=input,
256
+ timeout=timeout,
257
+ text=text,
258
+ )