Skip to main content

pytruenas

Version Python versions License: MIT Docs CI

A typed, Pythonic client and CLI for the TrueNAS middleware API. Every middleware method, attribute-style, over the middleware's own JSON-RPC 2.0 websocket — from a remote host or on the NAS itself — with commands and files over whichever transport the target actually offers. Documentation: https://jose-pr.github.io/pytruenas/.

Features

  • The whole API, attribute-style. client.api.pool.dataset.query(...) is the middleware method pool.dataset.query; no per-endpoint wrappers.
  • Create-or-update helpers. _get/_query/_create/_update/_upsert diff against the current record and send only what differs.
  • Local and remote. wss:///ws:// to a remote host with an API key, password or token, or the root-owned unix socket (ws+unix://) with no auth on the NAS itself.
  • Extended JSON. datetime/date/time/set/IP-interface values round-trip through the middleware's wire format.
  • Commands and files. run() and path() come from hostctl: plain subprocess on the NAS, SSH, or the web UI's own shell for a host reachable only on the API port; SFTP or the middleware filesystem.* API for files.
  • A CLI for many hosts. query, call, dump-api, deploy and your own command modules, fanned out over host ranges such as 'nas[1-3]'.
  • Typings on demand. generate-typings writes .pyi stubs for a host's whole API, so editors and type checkers understand every call.

Installation

pip install pytruenas
Extra Enables
pytruenas[ssh] SSH commands + the SFTP filesystem leg (asyncssh)
pytruenas[yaml] A CLI config file written in YAML (#!yaml, .yaml); TOML needs no extra (anycfg[yaml])

Quick start

from pytruenas import TrueNASClient

# Remote host (api key, or "user:password", or a token)
client = TrueNASClient("nas.example.com", credentials="1-<64-char-api-key>", verify=False)

# Attribute-style access to any API namespace/method:
for user in client.api.user.query():
    print(user["username"])

# Convenience helpers for common DB patterns:
client.api.user._upsert(("username",), username="svc", full_name="Service", group_create=True)

# Running on the NAS itself talks to the local unix socket, no auth:
local = TrueNASClient()            # ws+unix:///var/run/middleware/middlewared.sock
print(local.api.system.info())

Commands and files

run() and path() are inherited from hostctl, which selects a transport rather than assuming one:

client = TrueNASClient("nas.example.com", credentials=api_key, shell="ssh://root@nas.example.com")

client.run("zpool status", capture_output="stdout", encoding="utf-8").stdout
client.path("/mnt/tank/notes.txt").read_text()

client.capabilities      # {"run", "path", ...} -- what this target can actually do
client.last_selection    # which transport served the last run(), and why
target commands files
on the NAS local (plain subprocess) local
remote, SSH configured ssh, then webshell sftp, then tnasws
remote, no SSH webshell tnasws

webshell runs commands over /websocket/shell — the same PTY the web UI's Shell page uses — so a host reachable on the API port but not on 22 (NAT, a firewall allowing only 443) can still run commands. Name providers explicitly with executor= / path= to force or exclude one.

Output is exact: commands and input travel base64-encoded and output is framed by markers the command prints, so the terminal's echo, prompt and banner never reach your result. Uncaptured stdout streams live; stderr is captured separately (delivered when the command ends); input=/stdin= are delivered through a temporary file, never typed into the terminal.

Credentials

TrueNASClient(target, credentials=...) accepts, as credentials= (a keyword):

  • an API key string "<id>-<64 chars>",
  • "user:password" (optionally "user:password\n<otp>"),
  • a token string,
  • a (user, password) tuple,
  • None / omitted → local socket auth.

Credentials.from_env() reads PYTRUENAS_CREDENTIALS. Credentials may also travel in the target (wss://root:secret@nas). In a URI an OTP follows the password after a newline as a keyed line — otp:<token>, with the newline percent-encoded: wss://root:secret%0Aotp:123456@nas. In the plain credential string above the token follows the newline on its own ("user:password\n123456").

A password containing /, ? or # must be percent-encoded (%2F, %3F, %23); a raw one is refused, because the URL parser would otherwise read the rest of it as a host, port and path.

Command line

pytruenas --help
pytruenas query user -f username=root nas.example.com
pytruenas call system.info nas.example.com          # any method by dotted name
pytruenas dump-api nas.example.com > api.json
pytruenas generate-typings --api-version v26.0.0 nas.example.com
pytruenas deploy nas.example.com                    # install pytruenas ON the host

The target host(s) are the trailing positional arguments — a command's own positionals (like query's namespace) come first, then the hosts. Each target may be comma-separated and supports [A-Z]/[0-9] range expansion (e.g. 'nas[1-3].example.com'); with no target the command runs against localhost. --parallel N runs several targets concurrently. Filter query with -f/--filter KEY=VALUE (repeatable).

API overview

Module Purpose
pytruenas TrueNASClient, TrueNASConfig, Credentials, Namespace, Event, Subscription, __version__
pytruenas.host The client: connection and login, client.api, events, and hostctl's run() / path()
pytruenas.namespace Namespace: middleware calls by attribute, and the _get/_query/_create/_update/_upsert helpers
pytruenas.connection The JSON-RPC 2.0 websocket transport and the extended-JSON codec
pytruenas.auth Credentials: API key, user and password (with OTP), token, local socket
pytruenas.fs Remote paths over the middleware filesystem.* API
pytruenas.query Filter operators (EQ, GT, IN, ...) and Option for _query/_get/_upsert
pytruenas.target, pytruenas.apicache Connection strings (Target, redact); the cached API definition
pytruenas.exceptions Every error, under TrueNASError
pytruenas.providers, pytruenas.webshell The transports behind run() and path(), the web shell among them
pytruenas.cli The CLI: main, its global options (PyTrueNASArgs) and output helpers for your own commands
pytruenas.cli.runpath step and default_init for RunPath flows (numbered step directories)
pytruenas.patch Changing a host beyond what the API exposes: templates, systemd units, the middleware itself
pytruenas.codegen, pytruenas.models The typings generator and the API-dump schemas it reads

Every export, with its signature and contract, is in the API header that ships inside the package, pytruenas/AGENTS.md, and in the API reference.

Running on the appliance

TrueNAS has a read-only root and no pip, so deploy bootstraps instead, through hostctl's hostctl.deploy: it asks the target which distributions it already has, bundles only the difference, and copies that over. The appliance already ships requests, websocket-client, pyyaml, asyncssh, jinja2 and packaging, so the bundle is pytruenas and its pure-Python dependencies duho, hostctl, netimps, pathapi and pathapi-uri, plus pathspec where the target lacks it.

pytruenas deploy nas.example.com                 # a single executable .pyz
pytruenas deploy --mode dir nas.example.com      # an unpacked bin/ + lib/ tree
pytruenas deploy nas.example.com -- call system.info   # install, then run there

Everything after -- runs on the target once it is installed. The default destination is under /var/db/system, which is a dataset on a data pool and so survives an update — unlike /var/db itself, /root or /data, which live in the boot environment and are replaced by one. A digest is recorded beside the payload, so redeploying verifies instead of re-copying; --force overrides. An existing --path that a previous deploy did not create is refused rather than replaced.

When pytruenas is a dependency of your own tool rather than the thing being deployed, name yours as the root: --pkg-root mytool (or PYTRUENAS_PKG_ROOT), and your package ships with pytruenas bundled underneath it.

--source repo ships a working tree as-is instead of an installed dependency closure — the files a clone would have, filtered by whichever of .gitignore, .ignore and .bundleignore are present, with nothing needing to be installed locally first. It requires --mode dir: a zipapp needs an importable package root, which a working tree does not have.

pytruenas deploy --source repo --mode dir --repo-root . nas.example.com

Typings generator

generate-typings turns a host's API definition into a package of .pyi stubs so editors and type checkers understand client.api.<namespace>.<method>(...). The test suite compiles the stubs it generates from a fixture dump and a set of unusual schema shapes and type-checks them with pyright and mypy; it does not run against a full live dump.

pytruenas generate-typings --path truenasapi_typings/current nas.example.com

Development

git clone https://github.com/jose-pr/pytruenas.git
cd pytruenas
py -V:3.14-arm64 -m venv .venv/3.14-nt-arm64
.venv/3.14-nt-arm64/Scripts/python -m pip install --only-binary=cryptography -e ".[dev,docs]"
.venv/3.14-nt-arm64/Scripts/python -m pytest -q -rs

That is the recipe on Windows ARM64. A venv is named .venv/<python>-<os>-<arch> after the interpreter it holds: on Linux, for example, python3.14 -m venv .venv/3.14-posix-x86_64, with bin/ where Windows has Scripts/. --only-binary=cryptography stops pip from building cryptography from source, which fails on Windows ARM64, where recent releases ship no wheel; on a platform with a wheel it changes nothing.

dev carries every extra that has tests. A test whose extra is missing skips, so a partial install still prints green: -rs lists what skipped. Some run() tests need a POSIX shell and skip on Windows, so verify anything touching command or path dispatch on a real target. Supports Python 3.9+; run the suite on 3.9 as well before a release. Formatting, docs and benchmarks: CONTRIBUTING.md.

Releasing

  1. Add the ## [x.y.z] - <date> section to CHANGELOG.md and set version in pyproject.toml (PEP 440: 1.0.0rc1 for the tag v1.0.0-rc.1) in the same commit. The release notes are scraped from that section.
  2. Run the suite on 3.9 and 3.14 and mkdocs build --strict; pushing a throwaway ci-* tag runs the whole test workflow.
  3. Push the vX.Y.Z tag. The release workflow tests, tests at the dependency floors, builds, smoke-tests the wheel and gates on the docs build before it creates the GitHub release, uploads to PyPI and deploys the docs. A tag with a - (a pre-release) stops at the GitHub release.

Before 1.0, the minor version changes only when the documented API breaks; additions and fixes are patch releases.

License

MIT — see LICENSE.

Metadata

Release files for pytruenas 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pytruenas 0.7.0
File Size Uploaded Provenance
pytruenas-0.7.0.tar.gz 444.5 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for pytruenas 0.7.0
File Interpreter ABI Platform
pytruenas-0.7.0-py3-none-any.whl Python 3 none any Details

Total release size: 662.6 kB

Release files / pytruenas-0.7.0.tar.gz

Download URL pytruenas-0.7.0.tar.gz
Size 444.5 kB
Tags Source
SHA-256 checksum
How to use checksums
1bd5ab48d929e7029afdf17bbad4ccb24a17ca825729ca5e5d239ae5c199d436
BLAKE2b-256 checksum
How to use checksums
ce64e3065caf6092814d20fa9640268feb13e259f0642f93d1c01ec7c3d6549a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Release files / pytruenas-0.7.0-py3-none-any.whl

Download URL pytruenas-0.7.0-py3-none-any.whl
Size 218.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8c8ac95a266b557b5d41587eca07c21af9b0f1086e5b463085ba7df0a073224e
BLAKE2b-256 checksum
How to use checksums
7cd2f9303d60be81999424c4144ca30544fbbbb17e440406c20453250b629b41
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Security for pytruenas 0.7.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for pytruenas-0.7.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page