Releases: jose-pr/hostctl
Release list
v0.2.2
Changed
-
Dependency ranges widened to admit
pathlib_next0.9 andnetimps0.2:
pathlib_next>=0.8.6,<0.10andnetimps>=0.1,<0.3, with thesshextra's
pathlib_next[sftp-async]bound moved to<0.10alongside it. The
previousnetimps<0.2also conflicted withpathlib_next0.9's ownuri
extra, which requiresnetimps>=0.2.0.The floors stay where they were rather than rising to the new minors:
hostctl uses no API added in either release — its three netimps functions
(get_default_port,is_local_address,try_parse) all exist in 0.1, and
ProgressReaderis hostctl's own wrapper, unrelated to pathlib_next 0.9's
nativecopy(progress=). A floor demanding versions the code does not need
would exclude working installs for nothing. The ceilings span two minors
because both projects are pre-1.0, where a minor may break.Verified against both ends of the range: the suite passes with
pathlib_next0.8.6 and 0.9.0, each alongsidenetimps0.2.0.
Full Changelog: v0.2.1...v0.2.2
v0.2.1
Added
-
ConnectionStringparses a connection target from whatever a user typed.
ConnectionString("nas", scheme="wss")andConnectionString("nas:8443", ...)parse, because a bare host is not an invalid URI — it is a URI with
the scheme left off, which is what people type on a command line.Every field can be supplied directly, and three layers decide each one: an
explicit argument wins, then whatever the string carried, thendefaults.
scheme=/port=/host=/… are therefore overrides —scheme="ssh"
beats awss://in the string — whiledefaults(a mapping, or another
ConnectionStringused as a profile) fills only what nothing else
supplied.A port carries its own resolution strategy wherever one is accepted: an
int, a callablescheme -> port | None, or anything indexable by scheme
(a plain mapping), so a caller passes its whole table without pre-selecting
an entry. When no layer supplies one,netimps.get_default_portresolves
the scheme — it knows schemes no system services database does (ws/wss,
socks5), and an application can register more with
netimps.register_port.Falsestops the search outright: no port, and no lookup.Credentials are parsed but never rendered.
str()andrepr()both emit
the redacted form, with the password removed rather than masked, so the
output stays a valid, reusable connection string that cannot round-trip a
wrong credential — and a value reaching a log line or a traceback frame
cannot leak one. A password may carrykey:valueextras after a newline,
asparse_credentialsdescribes, written raw.The host keeps the spelling it was given.
is_localresolves nothing — it
is called while a configuration is built, which is network-free, and
resolving would both block and raise on a name that does not resolve. An
address literal is answered bynetimps.is_local_address, so an address
actually assigned to this machine counts as local and not just loopback,
while a name is compared against the loopback spellings.qsland
query_val()read the query;replace()returns a changed copy.
Changed
netimpsis now a required dependency, supplying scheme/port and
address semantics rather than hostctl reimplementing them. Note this
arrives in a patch release: an existing install pinned within0.2.xgains
a new requirement, so a locked or offline environment needsnetimps
available before upgrading.
Fixed
- A path in a command is rendered through
__fspath__rather thanstr().
Every transport now shares onecommand_texthelper — the SSH, WinRM,
container, QGA, and PSRP executors each stringified withstr(), so only
the local executor asked for the filesystem representation.__fspath__is
tried by attribute rather than byisinstance(value, os.PathLike), so a
duck-typed path that never registered with the ABC is honoured too, and an
object whose__fspath__raises or returns a non-string falls back to
str()rather than failing the command. The shell layer follows the same
rule. No shipped path type changes behaviour —str()and__fspath__
agree for all of them — but the contract now matches what a path promises.
Full Changelog: v0.2.0...v0.2.1
v0.2.0
Added
Exec(program, *args)marks one command for direct execution: the program
runs with that argv and no shell layer renders. The program and every
argument may be astr,bytes, or a path object — all reach the transport
as text, so the spelling records how the caller held the value rather than
changing what runs. A bare program name is now possible
(Exec("ls", "-l")), resolved through the target'sPATH; it previously had
no spelling at all, because a plain string command is always shell text.- The full
run()command syntax is documented in the "Running commands"
guide: the varargs rule, the three command forms, operators, validation, and
the PowerShell rendering.
Changed
-
Breaking. Direct execution is marked by
Exec, not by position. A path
in the first argument used to mean "this is the executable, the rest is
argv"; a path is now an ordinary value that stringifies wherever it appears.host.run(Path("/bin/ls"), "-l") # before: direct execution host.run(Exec("/bin/ls", "-l")) # now
This is a silent change for the old spelling:
run(Path(...), *args)
still succeeds, but the values become several;-joined shell commands
instead of one program and its argv, so arguments are subject to shell
quoting and word-splitting. Anything passing a leading path must be updated;
there is no deprecation shim.What it buys, both unspellable before: several path commands in one call
(run(p1, p2)is two commands), and direct execution of aPATH-resolved
name.An
Execcannot be combined with other commands in one call — there is no
shell to join them with, and running only one would silently drop the rest.
It raisesTypeErrorrather than choosing. -
hostctl runpasses its operand as a singleExec, so the program is used
verbatim. It no longer converts the operand through the target shell's path
flavour, which means a bare name now resolves through the target'sPATH
instead of being treated as a relative path.
Full Changelog: v0.1.2...v0.2.0
v0.1.2
Supersedes 0.1.1, which was tagged but never published: a release-gate test
timed out on the Windows/Python 3.14 runner, so nothing reached PyPI. There is
no 0.1.1 release; its fix is included here.
Added
uri_hostname(parsed)returns the host as it was written in a URI, rather
than the case-folded spellingurlsplit().hostnameproduces. Use it in
_from_parsed_uriwherever a host is stored; presence checks can keep using
.hostname, since emptiness does not depend on case.
Changed
-
A config built from a URI now stores the hostname as typed, so
HostConfig("ssh://nasA")keepsnasAin.host, inconnection_uri, and
in theHostInfo.hostnamea system host reports without connecting. It
previously storedurlsplit's lowercased form, which meant the library
echoed a spelling the operator never wrote and left downstream code to
recover the original from the URI itself.Consequently
.hostis the spelling that was given, not a canonical form:
HostConfig("ssh://nasA")andHostConfig("ssh://nasa")no longer compare
equal, so code using a config or its host as a dict key or for
deduplication should casefold explicitly. Resolution is unaffected — DNS,
SSH, and WinRM all treat the two spellings as one name.
Fixed
redact_uri()no longer case-folds the hostname. Only the branch that
rebuilt the authority — the one taken when a password was present — adopted
urlsplit's lowercasedhostname, sonasArendered asnasawith a
credential and asnasAwithout one. The same host now renders one way
whichever branch runs, and log records stay greppable by the name the
operator typed. Redaction removes a credential; normalizing a host is a
separate concern and is left to the transport that resolves it.- The spawn conformance test no longer fails on a slow runner. It waited 10s
for a realpowershell.exe -NoProfileto start, exit, and be reaped, which
a cold Windows CI runner can exceed. The wait is a hang guard rather than a
performance assertion, so it is now 60s. Test-only; no library change.
Full Changelog: v0.1.1...v0.1.2
v0.1.0
First release. Alpha: the public surface is deliberately small (66 exported
names) and may still change, but everything documented here is covered by the
test suite on Python 3.9 through 3.14.
Added
- Protocol-agnostic
Hostcontracts with secret-safeHostConfigURI
dispatch, lifecycle management, normalized host information, and explicit
capability reporting. - Local, SSH, WinRM, Docker Engine container, and QEMU Guest Agent hosts with
buffered command execution andpathlib_next.Pathfilesystem backends where
the transport supports them. WinRM includes a Windows-semantic PowerShell
path backend; container and QEMU paths use archive and guest-agent file RPCs. - Explicit and auto-detected shell dialects (POSIX, Bash, Zsh, Fish, CMD, and
PowerShell), shared structured quoting, environment/cwd helpers, operators,
and persistent SSH/container sessions with optional terminals. - Raw serial and QEMU serial-console transports with validated UART settings,
exclusive process leases, stream lifecycle controls, and explicit
non-shell semantics until a console profile is supplied. - A dependency-free
hostctlcommand with run, path inspection/copy,
host-info, and interactive shell subcommands; passwords are accepted only
from the environment or a hidden prompt. - Optional native integrations: AsyncSSH/SFTP, pywinrm, Docker SDK, PySerial,
pypsrp, and libvirt QGA, each isolated behind a matching package extra. - Cross-host
pathlib_next.Path.copy()/PathSyncersupport with streaming
remote readers, executor-side checksums, fast stat checksums, and an explicit
progress-reader recipe. - Transport-independent POSIX, Windows, and IOS host semantics with ordered
executor/path provider selection and capability-safe fallback behavior.
Providers declare per-operation capabilities, so a read-only backend rejects
a mutation explicitly instead of falling through to another provider.
Selection traces record the candidates, probe result, chosen provider,
generation, policy, and pin, with credential-like values redacted. Composite
paths keep their provider collection and optional.via()pin through/,
joinpath,parent/parents,with_name/with_suffix,iterdir,
glob/rglob/walk, and open streams. See the "Systems and providers"
guide for the no-replay safety rule and provider-authoring contract. - Symbolic-link support on every path backend whose transport provides it.
symlink_to(target, target_is_directory=False)andreadlink()follow the
pathlib.Pathsignatures,readlink()reports the stored target verbatim,
andstat(follow_symlinks=...)stays consistent withis_symlink(). Local
paths delegate toos.symlink, SFTP uses the SSH backend's
symlink/readlink, WinRM issuesNew-Item -ItemType SymbolicLink
(normalizing the Windows elevation/Developer-Mode requirement to
PermissionError), and container paths ship aSYMTYPEtar member through
put_archive(). QGA paths raiseNotImplementedErrorbecause the guest
agent exposes no symlink RPC. Container reads now follow a symlink member to
its target instead of failing, without giving up streaming laziness. - Subprocess-shaped execution options, normalized transport errors, bounded
buffered file transfers, and Python 3.9+ typing support (Python 3.14 is the
default development interpreter). Shellis a context manager:with host.shell as session:opens one
default session and closes it on exit, the no-argument shorthand for
shell.session(). Re-entering a shell whose session is still open raises
rather than sharing or leaking a process.Shellcarries defaults.host.shell(cwd=..., env=..., encoding=..., errors=...)returns a shell applying them to every laterrun/session
that omits its own value;configure(...)derives a further-configured copy
without mutating the original.cwd/encoding/errorsare replaced by a
per-call value,envmerges per key so one variable can change without
restating the rest, andenv=Nonedeclines the shell's environment while
keeping whatever the host itself provides.- Connection URIs may carry credentials.
scheme://user:password@hostis
accepted: the password is extracted into the credential arguments and
stripped from the parsed authority, so it never reaches a field that
connection_uriorrepr()renders.redact_uri()removes a password and
returns a valid, reusable URI rather than masking it, so a rendered form can
never round-trip a wrong credential. parse_credentials()splits a password field on a newline into the password
and trailingkey:valueextras, so an OTP or other second factor travels
through a single field. A bare name is a flag equivalent toname:. Inside
a URI the separator may be written raw — tab, CR, and LF are percent-encoded
before parsing, sinceurlsplitdeletes them silently. A control character
in the host is rejected, because deletion there rewrites the target.ssh_providers()andwinrm_providers()return an executor/path provider
pair sharing one transport, for composing a transport into a host you
assemble yourself.strict_uri_credentials,strict_uri_query, and
uri_hostare public for configs implementing_from_parsed_uri.- A
HostConfigsubclass may declareuri_credentials; dispatch then rejects
any other credential before construction, so a typo fails loudly instead of
silently producing a config with no password. - SSH execution now sends EOF for omitted stdin, rejects unsupported zero
buffering, normalizes missing exit statuses to-1, performs best-effort
timeout cleanup (withTimeoutExpired.orphaned), and normalizes persistent
process I/O failures. SFTP backends are reused per host and invalidated on
close; auto-dialect probes preserve the discovered shell executable. - Container archive paths now accept normal absolute and relative symlink
targets, resolve links with a bounded hop count, preserve hardlink/file
semantics, use Docker's header stat metadata where available, and reject
traversal names without buffering directory archives unnecessarily. Docker
exec streams return available data promptly, detect truncated frames,
preserve merged output ordering, and map missing containers to
ConnectionError. - QEMU Guest Agent transports now share one framed-session implementation with
split-read buffering, parse-error correlation, safe timeout/disconnect
cleanup, and loop-bound SSH writes. QEMU serial consoles and raw serial
processes use incremental text decoding and the commonread(-1)contract
(up to 64 KiB available data); serial ownership and QEMU URI/lifecycle edge
cases are normalized consistently.
Fixed
run(input=<bytes>, encoding=...)no longer hangs. Handing bytes to a
text-modesubprocessstdin killed its writer thread, and the call then
blocked forever because the child never saw EOF —timeout=did not fire.
inputis now normalized to the stream mode each executor uses, by a helper
the local, SSH, and QGA executors share, so the same call behaves
identically whichever provider aSystemHostselects.- Composite host paths kept their provider, selector, and pin through
pathlib.PurePathderivations (parents,with_name,with_suffix,
relative_to). Those results were previously built without any routing
state, which madeglob(),rglob(), andwalk()fail outright. - A path provider that declined before dispatch is remembered for the
connection generation instead of being re-attempted by every later
operation;invalidate()clears the record along with cached probes. SystemHostserializes its connection bookkeeping under a reentrant lock.
Concurrentrun()calls previously raced the check-then-append in
_ensure_provider_connected, so every caller repeated the connect
round-trip and appended a duplicate entry to_connected_providers, which
grew without bound. Provider membership is now tested by identity.- Capability vocabularies agree.
ExecutorCapabilitymembers subclassstr,
so the enum published by executors and the strings published by providers
and hosts compare equal.Shellpreviously testedExecutorCapability.CWD
against a set of strings — always false — so a host with nativecwd/env
still hadcd/exportrendered into the script and the native values
dropped. SystemConfigno longer fails withAttributeError. It is explicitly
abstract:_create_host()raisesTypeErrornaming the concrete
configurations, and it no longer advertises asystem://URI that
HostConfigrejected as an unsupported scheme.schemeis a URI-derived property across the wholeHostConfighierarchy.
The system configurations shadowed it with a plain string andSystemHost
assigned to it; a config-less host now builds its own family configuration
instead.
Full Changelog: https://github.com/jose-pr/hostctl/commits/v0.1.0