Releases: jose-pr/pytruenas
Release list
v0.3.1
Fixed
hostctl<0.2made the dependency set unresolvable.pathlib_next
0.9.0'suriextra requiresnetimps>=0.2.0, andhostctlwidened its own
ranges to admit that in 0.2.2 — so pinninghostctl<0.2left no solution for
apip install pytruenasthat also pulledpathlib_next[uri].os.fspath()on aTruenasPath/TnasWsPathraised
NotImplementedError. Both now set_host_filesystem_path, sofspath
returns the host-local path (/usr/lib/x, not
truenas://root@nas/usr/lib/x). Requirespathlib_next>=0.9.0.
Changed
patch.zfs.host_pathusesos.fspath()only. It previously fell back to
.path, thenas_posix(), thenstr(), forpathlib_next<0.9where
fspathraised for every non-filescheme. A URI scheme with no host-local
path now raises rather than yielding a string that is not a path.
Dependencies
hostctl>=0.1.2,<0.3(from<0.2). Nothing pytruenas imports changed;
the floor stays at 0.1.2 because no 0.2 API is used here. Validated on 0.2.2.pathlib_next[uri]>=0.9.0,<0.10(from>=0.8.2, uncapped) — for
UriPath.__fspath__on host-filesystem schemes, and for
Source.__str__/__repr__no longer emitting the password. Thessh
extra'spathlib_next[sftp-async]moves to>=0.9.0,<0.10alongside it.netimps>=0.0.2,<0.3— ceiling added; 0.2.0 reworkedresolve()and
madednspythonoptional, neither of which touchesget_default_port, the
only function used here. Validated on 0.2.0.
Full Changelog: v0.3.0...v0.3.1
v0.3.0
Run pytruenas on the appliance, and patch things the API does not expose.
pytruenas.ops is now pytruenas.patch, restructured into subpackages, with
ops.host and ops.main removed (see "Removed"). Renaming it is why this is
0.3.0 rather than 0.2.3 — though in practice there was nothing to break: the
subpackage was experimental, unexercised, and most of its paths simply did not
work. MiddlewareFiles referenced an attribute that never existed, systemctl
calls ran four commands instead of one, and creating a file with a baseline
raised. Those are all fixed below; the rename is the part worth noticing.
Added
pytruenas deploy <target>— installs pytruenas onto a host that cannot
install for itself. TrueNAS has a read-only root and nopip, so this probes
what the target already has, bundles only the difference, and copies that
over. In practice five pure-Python packages under a megabyte: the appliance
already shipsrequests,websocket-client,pyyaml,asyncsshand
jinja2, and which it ships varies by release, so it is asked rather than
assumed.--mode pyz(default) writes one executable zipapp;--mode diran
unpackedbin/+lib/tree, swapped into place via a staging directory
so an interrupted transfer never leaves a half-written tree.- Everything after
--runs on the target once installed:
pytruenas deploy nas1 -- call system.info. - Defaults to
/var/db/system, which is a dataset on a data pool and
survives an update — unlike/var/dbitself,/rootor/data, which
live in the boot environment and are replaced by one. Verified on a live
appliance rather than assumed. - A digest is recorded beside the payload, so a redeploy verifies instead of
re-copying;--forceoverrides. --pkg-root/--pkg-name(orPYTRUENAS_PKG_ROOT/PYTRUENAS_PKG_NAME)
deploy your distribution with pytruenas bundled underneath, for when
pytruenas is a dependency rather than the deliverable.
pytruenas.utils.bundle— the machinery behind it, deliberately generic:
it knows nothing about pytruenas or TrueNAS and could be lifted out. Resolves
a transitive closure from installed distribution metadata (not an import
scan —import yamldoes not namepyyaml), and refuses a distribution
carrying a compiled extension or resolving to data files only, both of which
would build cleanly and fail to import on the target.pytruenas.patch.zfs—writable(client, path)clearsreadonlyon the
backing dataset and restores it however the block exits. Required for
anything under/usr.dataset_forwalks up to an existing ancestor,
becausefindmnt --targetfails on a path that does not exist yet.- Undo, for the patch system that promised it.
FileTarget.revert()
restores the baseline and clears the snapshot;is_patched()reports whether
a file currently differs from its baseline;would_change(content)is the
dry run. A file the patch created is deliberately left alone — with no
baseline there is nothing proving it was ours to delete. SystemFile(..., writable=True, mode=...)— patch a read-only mount
without remembering the ZFS dance, and set the mode for a file the patch
creates.
Fixed
- A rewrite no longer widens a file's permissions. Rewriting reset the mode
to whatever the umask gave, so patching/etc/shadow(0640 root:shadowon
a real box) would silently have made it0644. The existing mode is now
captured before the write and restored after. systemctlwas invoked as four separate commands.run(*cmds)treats
each positional as its own command, sorun("systemctl", "disable", "--now", name)ran four — with the unit name hand-quoted on top, which argv does not
want. Every call now builds one argv list.is-active/is-enabledcould raise on the non-zero exit that is their
answer, so asking "is this running?" about a stopped service threw.services="nfs"iterated into three per-character reloads.mkdir(755, ...)passed decimal 755 —0o1363, setuid plus wrong bits —
on every directory it created.baseline=Trueon a file that does not exist yet raised
FileNotFoundErrorfrom insidewrite(): it tried to snapshot a missing
original. That is the ordinary "create this config if absent" case.baseline()calledresolve(), which hostctl'sCompositePosixPathdoes
not have, so any baseline against a real client path crashed.BaseTemplate.renderreturnedNonefor a subclass that forgot to
override it, and thatNoneflowed intowrite()as the file's content. It
now raises, andwrite()refusesNoneoutright.MiddlewareFilesused aclient.middlewared_paththat never existed.
Replaced with a lazy lookup that asks the host's own interpreter — no API
method reports it (checked against 26.0's 781 methods).find_templatedefaulted to taking a baseline beside a template on a
read-only mount, which cannot work; it failed with a bareOSErroron
first read.apply_template(**kwargs)raisedTypeErrorfor an already-built
template and silently dropped the kwargs elsewhere.
Changed
PYTRUENAS_*settings are read through oneduho.envaccessor. The app
had grown three names for two settings (PYTRUENAS_CFG,PYTRUENAS_CONFIG,
PYTRUENAS_PATH);PYTRUENAS_CONFIGis now canonical withCFGstill
accepted, and the set is enumerable rather than grep-discoverable.pytruenas.ops→pytruenas.patch, split intotemplates/,systemd/,
middleware.py,zfs.py. The old name described neither what the code does
nor what it costs: it modifies a host beyond what the middleware API
supports, which is unsupported by definition and discarded by a boot
environment swap on update.ops.midclt→patch.systemd(midcltis TrueNAS's own CLI binary, which
nothing in the module invokes).TruenasSystemFile→SystemFile,
SystemdUnit→Unit,SystemdServiceUnit→ServiceUnit,
MiddlewareCode→MiddlewareFiles.- Unit
enable/startare now three-valued:Nonemeans "not mine to
manage", which a bool could not express.
Removed
pytruenas.ops.host— its packaging half became
patch.templates/utils.bundle; the remaining network helpers
(is_localhost,is_local_ip,find_adapter_in_network) were thin wrappers
overnetimpsandipaddressthat nothing in the package imported. Call
netimps.interface_for/get_interfacesandipaddressdirectly.pytruenas.ops.main.init— imported by nothing, duplicated the CLI's own
config loading, and documented apytruenas.clientmodule deleted in 0.2.0.
Nowexamples/simple_client_from_yaml.py, as a worked example.
Dependencies
hostctl>=0.1.2— foruri_hostname(), which returns a URI's host as
written rather than case-folded.
Full Changelog: v0.2.2...v0.3.0
v0.2.2
Log records now identify the host they came from, and no longer carry the
connection string that used to be the only identification.
Released as a patch: the public surface only gains (client.name,
config.name, TrueNASWSConnection(logger=...)). The one output change is to
pytruenas.utils.target's redact/Target.redacted, an internal utility
module — not exported from the package root, not in the docs, and reached only
by explicit deep import.
Changed
- Log records identify the host by name, not by connection string. Every
record carried the full URI — scheme, port, API path and any userinfo — which
was unreadable at fan-out width and put the password on every line. Records
are now prefixed with the machine's short name ([nas1],[nas1:8443],
localhostfor the unix socket), available asclient.name/
config.name. Because the prefix always names the target, the per-target
Started:/Finished:messages no longer repeat it. - Attribution no longer depends on the CLI.
client.loggeris now bound to
the host's name, so a library caller with several clients open gets
attributed records with no fan-out involved — previously only the CLI's
duho.fanoutadded a prefix, and a bare "Websocket connection was closed"
did not say which host closed.TrueNASWSConnectionacceptslogger=and
the host passes its own, so transport-layer records are attributed too. - Redaction removes the password instead of masking it.
pytruenas.utils.target.redactandTarget.redactedrendered
wss://root:***@nas; they now renderwss://root@nas. A***placeholder
is not a real password and would reparse into a wrong credential if the
rendered form were ever fed back in, whereas what comes out now is a valid,
reusable URI.redactdelegates tohostctl.host.redact_uri, so pytruenas
and hostctl render a target identically. Note this is only for rendering a
raw connection string: credentials are extracted at parse time, so
config.connection_uriwas already credential-free. --logtofilenames use the short name.{target}now expands tonas1
rather than a full URI, which is both credential-free and a legal filename —
a URI is neither.
Fixed
- A hostname's typed casing is preserved in its label.
urlsplitfolds
case, so a fan-out overnasA/nasBlogged[nasa]/[nasb]. Correct for
resolution, wrong for a label an operator greps for. - The same host renders the same way with or without a password. A target
whose URI carried a credential logged as[nasa]while the identical host
without one logged as[nasA], because hostctl rebuilt the authority from
the case-folded hostname when stripping the password. Filed upstream and
fixed across hostctl 0.1.1/0.1.2.
Dependencies
hostctl>=0.1.2(was>=0.1.0) — foruri_hostname(), which returns a
URI's host as written instead of asurlsplitcase-folded it.
TrueNASConfigstores that, since the value is rendered back out through
nameandconnection_uri. Both hostctl releases came from a finding filed
from this repo. Note the trade-off inherited with it:config.hostis
now the spelling the caller typed rather than a canonical one, so two
spellings of one machine are not equal configs — casefold explicitly if you
key on a host. Routing is unaffected.
Full Changelog: v0.2.1...v0.2.2
v0.2.1
Documentation and one error-message fix. No API changes.
Fixed
- An unknown constructor argument now raises a useful error. A typo like
TrueNASClient("wss://nas", passwrd="s3cret")fell through to hostctl's
SystemHost.__init__and raisedTypeError: SystemHost.__init__() got an unexpected keyword argument 'passwrd'— accurate, but naming an internal
class rather than the thing the caller got wrong. It now raises
ValueError: unknown credential argument: 'passwrd'and lists the accepted
configuration options. Found by testing the documented examples.
Documentation
- README rewritten for 0.2.0: it still advertised a
pytruenas[host]extra
that no longer exists, said "once published to PyPI", and used a venv layout
the project no longer follows. Adds the transport table and a
commands/files section. - New Recipes guide —
worked examples for connecting, querying, idempotent upserts, subscriptions,
running commands, upload/download, several hosts at once, and provisioning
SSH. Every example was run against a live TrueNAS 26.0.0-BETA.1 host. - Filesystem guide expanded with real path examples and a table of which
operations need SFTP (rename,symlink_to,readlink,resolvehave no
filesystem.*equivalent and raise rather than failing quietly). docs/index.mdextras list corrected; both CI workflows dropped a stale
hostextra that pip had been silently warning about.
Full Changelog: v0.2.0...v0.2.1
v0.2.0
Rebases pytruenas' generic host machinery onto [hostctl], keeping only the
TrueNAS-specific parts here: the middleware websocket, the api namespace,
login/2FA, subscriptions, and the upload/download side channels. Everything
else — shell quoting, transport selection, the asyncssh lifecycle, path
backends — is now inherited.
Requires hostctl>=0.1.0,<0.2.
Changed
- BREAKING:
TrueNASClient.shellis gone..shellnow means what it means
throughout hostctl — the bound shell object (client.shell.run(...)). The
SSH connection target lives on the configuration asclient.config.ssh, an
SshConfig. The constructor argument is still spelledshell=and still
takes a connection string (shell="ssh://root@nas"). .run()and.path()now select a transport rather than branching.
Which one serves a call is chosen from the available providers, and
.last_selectionrecords what was tried and why — with credentials redacted.
Previously.run()hard-coded a local-vs-SSH branch andTruenasPath
hand-rolled its own SFTP→websocket fallback.- A remote target with no SSH can now run commands over the web shell. The
TrueNAS JSON-RPC API exposes no remote command execution (verified against
26.0.0-BETA.1: of 781 methods onlycore.resize_shelland
user.shell_choicesare shell-adjacent, and the former only resizes an
already-open session) — so such a host previously had norun()at all.
/websocket/shell, the PTY the web UI's Shell page drives, is a real command
channel on the same port. Passexecutor=["ssh"]to require SSH instead. - The scheme/API-path probe moved from construction to first connect.
TrueNASClient("bad-host")now constructs successfully and raises on first
use. Configs are therefore buildable offline, which is whatHostConfig
requires.
Added
pytruenas.host—TrueNASConfig(ahostctl.host.HostConfig) and
TrueNASHost(aPosixHost).HostConfig("truenas+wss://nas")resolves
through hostctl's registry; every connection stringTrueNASClientaccepts
still works, normalized to atruenas+*scheme.TrueNASClientandTrueNASHostare now one class. They were briefly
two objects that forwarded halves of their surface to each other —
client.run()calledclient.host.run()whilehost.apicalled
host.client.api, each holding a reference to the other.TrueNASClientis
an alias forTrueNASHost, so every existing import and call keeps working,
andclient.host/host.clientboth return the object itself.
TrueNASHost("wss://nas")also takes a connection string directly, with the
same options asTrueNASConfig.from_target.pytruenas.providers—TnasWsPathProvider(thefilesystem.*
websocket leg) andlocal_providers(), which returns hostctl's stock local
executor and path providers unchanged. A local target runs plain
subprocessand uses plain local paths; there is nothing TrueNAS-specific to
add, so pytruenas defines no provider class for it.pytruenas.webshell—WebShellExecutorProvider, command execution over
/websocket/shell. Ordered after SSH; declares its limits rather than hiding
them (stdout and stderr are one stream, no piped input, single-line commands
only — pipes and here-strings work, being ordinary shell syntax).executor=/path=onTrueNASConfig— name the providers to use, in
preference order, as a single name or a sequence:executor=["ssh"],
path=["local", "tnasws"],executor=[]for no command channel at all.
Unknown names, andssh/sftpwithout anSshConfig, raise rather than
composing a host that would fail later. Matches hostctl's own
SystemConfig(executor=..., path=...)spelling.Credentials.from_host_credentials()— maps hostctl's already-parsed
credential mapping (including a URI-supplied OTP) onto aCredentials
subclass, with no second round of string parsing.- Inherited from hostctl:
.capabilities(so a host that genuinely cannot
run commands says so up front rather than failing mid-call),
.last_selection,.info(),.spawn(),.connect()/.close(), and
context-manager support. blackin thedevextra, pinned to the 3.9 floor.
Removed
- BREAKING:
pytruenas.jsonrpcis nowpytruenas.connection, and its
Clientclass isTrueNASWSConnection.Clientwas doubly wrong: the class
is not generic JSON-RPC (it knowscore.subscribe, TrueNAS error codes, and
the middleware unix socket), and the name collided withTrueNASClient.
client.connis the connection, with.websocketkept as an alias. - BREAKING:
pytruenas.clientandpytruenas._connare gone.
from pytruenas import TrueNASClientis unaffected._connwas a re-export
shim for swapping the client implementation, which never happened;client
had been reduced to an alias by the host/client merge. - ~190 lines of generic host machinery: shell quoting, the local-vs-SSH branch
inrun(), the asyncssh connection, and_shellquote.
Known limitations
- A local unix-socket client cannot use
download(). The HTTP side channel
resolves tohttps://localhostand trips the appliance's self-signed
certificate. This is pre-existing and unrelated to the migration — the URL
construction is byte-identical to 0.1.1. - The web shell merges stdout and stderr (a PTY is one stream), takes no
pipedinput=, and requires single-line commands. Pipes and here-strings
work, being ordinary shell syntax. It is ordered after SSH for these reasons.
Full Changelog: v0.1.1...v0.2.0
v0.1.1
Changed
call/query/generate-typingsdeclare fewer field options. Their
Argsclasses no longer passNS(type=...)for a plainstr/Pathfield
(duho already derivestype=from the annotation) orNS(action='append', nargs=...)for alist[str]option (duho >=0.5.0 already defaults a
list-typed option toaction="append",nargs=None— one value per
occurrence). OnlyNS(metavar=...)remains where the display name isn't
inferable. No CLI-surface change.- An option placed between a command's own positional and the trailing
targets now parses, e.g.pytruenas call method -p '{"a":1}' nas1— no
longer only before the first positional or after the last. Was argparse's
own greedy positional-run matching (bpo-15112); fixed by duho >=0.5.1's
flag-between-positionals reorder, extended in 0.5.1 to a module command's
subparser (this project's entire command set — 0.5.0 alone only covered
duho's own declarative subcommand tree). - Dependency floor
duho>=0.4.1→duho>=0.5.1, required for both
changes above.
Full Changelog: v0.1.0...v0.1.1
v0.1.0
First published release with real content. 0.0.0 was a placeholder; everything
below accumulated since and is new to PyPI.
Added
- Modern
auth.login_exlogin with 2FA.client.login(login_ex=True)uses
the middleware'sauth.login_exmechanism (PASSWORD_PLAIN/API_KEY_PLAIN/
TOKEN_PLAIN) instead of the legacyauth.login/login_with_*. It handles an
OTP_REQUIREDchallenge by continuing withauth.login_ex_continue— the OTP
comes from the credential'sotp_tokenor anotp_providercallback — and
raisesauth.AuthenticationErroronAUTH_ERR/DENIED/etc.login_options
overrides the server defaults. The legacy path remains the default and
unchanged; a credential with no login_ex form (local-socket auth) falls back
automatically. Validated live against TrueNAS 26.0. - Client convenience wrappers
client.me()(auth.me),client.logout()
(auth.logout), andclient.ping()(core.ping). - Event subscriptions. Subscribe to middleware collection events over the
existing websocket:client.subscribe("alert.list")(or
client.api.alert.list.subscribe()) returns aSubscription. Consume events
by iteratingsub.events(timeout=...)— a bounded queue drained on the
caller's thread, so backpressure is visible; a full queue drops the oldest
event and counts it insub.droppedrather than blocking. An optional
callbackis invoked inline on the reader thread (keep it fast; a raising
callback is logged and contained). Each event is anEvent(collection, msg, fields, id). Close withsub.unsubscribe()or awithblock; closing the
client ends everyevents()iterator cleanly. A subscription is bound to the
current connection and does not survive a reconnect — theevents()
iterator ending is the signal to re-subscribe. Validated live against
TrueNAS 26.0. - RunPath step directories. Adopt
duho.runpath(requiresduho>=0.4.0),
wired into the per-target fan-out: a directory of numberedNN-name.pysteps
(no__init__.py), placed among the command sources (PYTRUENAS_PATH/
--cmdspath/ configcommandspath, or nested one level inside a source
directory), becomes a subcommand that runs the whole step sequence once per
target, each target getting its own connectedTrueNASClient— restoring
the private predecessor's per-targetRunPathCmdbehavior the current
duho-basedpytruenasnever had. A directory's optional__main__.py
init(cmd, logger)builds the per-target client (reuse
pytruenas.utils.runpath.default_init); steps aremain(cmd, ctx)/
main(cmd);-O/--rcoptsand filename!/!strict/!enabletokens select
steps. The step signature isduho's nativemain(cmd, ctx)rather than the
predecessor'srun(client, args, logger)(the logger travels oncmd, the
client isctx) — capability parity, not signature parity. The
filename-modifier /--rcoptsgrammar follows the predecessor's intent with
two of its original bugs fixed (the:!enable/.enabledattribute mismatch,
and theExtend()nested-list double-collection), not reproduced.
Changed
- Local network-adapter discovery uses
netimpsinstead ofifaddr.
pytruenas.ops.host.is_local_ip/find_adapter_in_networknow delegate to
netimps(a core dependency), so the optionalhostextra is removed —
those helpers work out of the box, nopip install pytruenas[host]needed.
find_adapter_in_networknow returns anetimps.Interface(was anifaddr
adapter). Requiresnetimps>=0.0.2, which also supplies thews/wssdefault
ports built-in, soutils/target.pyno longer registers them at import. - The trailing
TARGET...positionals are registered centrally. Every
command'sregisterhook previously had to call
pytruenas.utils.cmd.register_targets(parser)last or silently lose the
target grammar.pytruenas.mainnow wraps each command's hook and adds the
positional after it, so targets stay trailing whatever positionals a command
adds — including for a command with noregisterhook at all, and for
third-party commands supplied via--cmdspath/PYTRUENAS_PATH, which now get
the<command> [args...] [TARGET ...]grammar for free. - Commands declare their CLI fields via an
Argsclass.call,queryand
generate-typingsdeclare arguments on theirArgsclass rather than adding
them imperatively inregister()(which they no longer define). Previously the
Argsclass was inert — duho ignored it,register()did the real work, and
the two were hand-synced.register()remains supported as the escape hatch
for what declarations can't express. The CLI surface is unchanged. - Dependency floor
duho>=0.4.0→duho>=0.4.1for the two behaviors the
above depends on: a module command may declare its ownArgsclass (added to
the subparser beforeregisterruns), and theregisterhook is gated and
introspected on the object actually called, so wrapping it app-wide works even
for a command that defines no hook of its own. - Dependency floor
duho>=0.3.2→duho>=0.4.0. 0.4.0 carries the RunPath
register(base=...)shared-root method inheritance, the__main__.py
lifecycle, the correctedenable/!enabletoken spelling, and theExtend()
nested-list fix that now flattens--cmdspath a:bto['a', 'b'](previously
silently mis-collected as[['a', 'b']]for multi-value input).
Security
- Passwords in a target connection string are redacted from logs. A target
likewss://root:secret@naspassed as a positional was logged verbatim
(Started: …/Finished: …, at INFO) and, worse, embedded in the--logto
filename on disk. The password is now masked (wss://root:***@nas) at every
such point viapytruenas.utils.target.redact— the username is kept, the
real target still builds the client. Theauth.Credentials"not supported"
ValueErrorno longer carries the rawpassword/token/api_keykwargs
(which anexc_info=Truelog would have surfaced). Command text logged by
client.runis unchanged: that logging is intentional, opt-in vialoglevel
(defaultTRACE, off unless enabled), and suppressible withloglevel=0.
Fixed
- Connection-string reassembly preserves reserved characters.
Target.uri
now percent-encodes userinfo and path, so a credential or path containing
@ : / #round-trips instead of reassembling into a URL that reparses to a
different host/port/path. opsreads files as UTF-8, and narrower exception handling inauth
(ValueError/TypeErrorrather than bareexcept Exception) so a genuine
error surfaces instead of being swallowed behind a generic message.
Full Changelog: v0.0.0...v0.1.0
v0.0.0
Initial release.
Earlier version numbers appear in this project's git history but were never
tagged or published, so there is no upgrade path to describe -- everything
below is simply what the package contains.
Fixed
ws://andwss://URLs no longer parse as port 0. No system services
database has an entry for the websocket schemes, sogetservbyname("wss")
failed -- and those are the schemes this client uses most. Port resolution now
goes throughnetimps, whose scheme table is consulted before the system
database.
Added
pytruenas call <method>command. Invoke any middleware method by its
dotted name (system.info,core.ping,pool.dataset.details) — not just
the queryable<namespace>.querymethodsquerycovers. Parameters are JSON
values via-p/--param(repeatable).
Changed
-
CLI targets are now trailing positional arguments, not
-t/--target.
A command's own positionals come first, then the target host(s):
pytruenas query user nas1 nas2,pytruenas dump-api nas1,nas2. Comma lists
and[A-Z]/[0-9]range patterns still expand; no target meanslocalhost.
The-t/--targetflag has been removed. -
Dependency floors raised to the validated versions:
duho>=0.3.2(CLI parser
fixes — a global option before a subcommand is no longer shadowed; a literal
%in aCmddocstring no longer breaks parser build) and thesshextra's
pathlib_next[sftp-async]>=0.8.3(SFTP default concurrency raised 8→16).
Fixed
-
API calls no longer silently return
Noneon a dropped connection. The
namespace call retry loop fell through and returnedNoneafter a single
ECONNABORTED— which_getread as "record missing", turning an_upsert
into a spurious create (possible duplicate rows). It now retries then raises,
and never returnsNoneon a connection error. -
Long-running jobs no longer spuriously time out.
core.job_wait(waited
on after uploads/downloads and mutating_upsert/_updatecalls) is now
issued with no client-side timeout, so a job lasting longer than the 60s
default no longer raisesCallTimeoutwhile it is still running server-side.
Client.call(timeout=None)now means "wait indefinitely". -
client.run()with astrinputtogether with a textencoding/errors
no longer crashes. It used to pre-encode the string to bytes and hand the
encoding tosubprocess.run, which then tried to.encode()the already-bytes
input (AttributeError). Now text mode keepsstrinput as-is (and decodes
bytesinput), binary mode encodes. Found by live testing on TrueNAS 26.0. -
ops.template.TemplateTarget.apply_templateno longer crashes on a plain
string template (issubclass()was called on a non-type); astris now
treated as literal template content and a path-like is read as file content. -
namespace.ioerroronly maps a middleware error toOSErrorwhen the
bracketed prefix names a real POSIX errno; previously an unrecognised prefix
producedIOError(None, msg), discarding the original exception type.
Internal
jsonrpc.Client.callnarrows the compatibility kwargs it ignores and logs any
other unexpected keyword at debug level instead of silently swallowing it;
_ioerroris no longer forwarded into the upload/download paths.Namespacechild lookups use a per-instance dict instead offunctools.cache
on the methods, so namespaces are garbage-collected with their client instead
of being pinned for the process lifetime (relevant to long-lived embeddings).- The
pytruenas.opssubpackage (systemd/midclt host-config helpers) is
experimental and exercised only by unit tests, not against a live host.
Added
- Packaged as
pytruenas(src layout, hatchling,pytruenasconsole script,
py.typed). Python 3.9+. - Lean in-house JSON-RPC 2.0 client (
pytruenas.jsonrpc) speaking the middleware
protocol overwss:///ws://and the localws+unix://socket, with
extended-JSON (datetime/date/time/set/IP) round-tripping and
ClientException/ValidationErrorsmapping. Verified against a live host. - Attribute-style API namespace (
client.api.<namespace>.<method>(...)) with
_get/_query/_create/_update/_upsertconvenience helpers. - Filesystem paths on
pathlib_next:client.path()returns aLocalPath
(local) orTruenasPath(remote — SFTP-preferred via pathlib_next'sSftpPath,
falling back to the middlewarefilesystem.*websocket API for
delete/rename/symlink). - Typings generator (
generate-typings): produces.pyistubs for the whole
API, validated to parse across every version of a real v26 dump (780 methods). - CLI (
dump-api,query,generate-typings) onduhowith multi-target
fan-out (-t/--target,--parallel) and optional YAML config. - Optional extras:
ssh,config,codegen,host. - Test suite green on Python 3.9 and 3.13/3.14.
Notes
- Runtime CLI/logging/qualname/text come from
duho(>=0.3.0); path types from
pathlib_next(>=0.8.2). Both are on PyPI. - Remote shell command execution (
client.runover SSH) usesasyncssh(the
sshextra); the middleware API has no command-exec method. SFTP is handled by
pathlib_next.
Full Changelog: https://github.com/jose-pr/pytruenas/commits/v0.0.0