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