The API namespace
client.api is a dynamic namespace: attribute access builds a dotted method
name, and calling it invokes that middleware method.
client.api.system.info() # -> system.info
client.api.pool.dataset.query([["type", "=", "FILESYSTEM"]]) # -> pool.dataset.query
Any method the middleware exposes is reachable this way — there are no per-endpoint wrappers to keep in sync with TrueNAS.
Convenience helpers
On top of the raw methods, each namespace offers helpers (prefixed _) for the
patterns you'd otherwise hand-write:
| Helper | Does |
|---|---|
._query(**filters) |
query with filters built from keyword args |
._get(id) / ._get(**filters) |
one record by id or filter, or None |
._create(**fields) |
create |
._update(selector, **fields) |
update, diffing against current state |
._upsert(selector, **fields) |
create if absent, else update the diff |
# Filters from kwargs, with operator helpers:
from pytruenas import query as q
admins = client.api.user._query(uid=q.GT(0), locked=False)
# Fetch-or-None:
root = client.api.user._get(username="root") # dict, or None if missing
# Create-or-update by a selector field:
client.api.user._upsert(("username",), username="svc", full_name="Service")
_upsert(selector, ...) resolves the row by the selector field(s) — a tuple
or list of field names, matched against the values you pass; if it exists the
changed fields are updated (a no-op diff makes no call), otherwise the record is
created. A bare string or int selector is a record id, not a field name
(_upsert("username", ...) looks up the record whose id is "username"). A
!-prefixed name (("username", "!uid")) is left out of the match and is not
changed on update; a selector made only of those is rejected, since it would
match the first record in the collection. When the create/update method
is a middleware job — its schema says so (core.get_methods, read once per
namespace) — the job is waited on by default (wait=True; pass a callable
instead to receive progress updates) and its result returned. An integer
answer from a method that is not a job is a record id, returned as it is.
{"job": True} (or False) states it yourself and skips the lookup.
Waiting for jobs
Many middleware methods start a job and return its id. client.wait(job_id)
blocks until it finishes and returns its result; a failed job raises
pytruenas.connection.JobFailed. Pass callback= to follow progress, or
iterate client.job_updates(job_id) yourself:
job_id = client.upload(data, "filesystem.put", "/mnt/tank/big.bin", wait=False)
client.wait(job_id, callback=lambda job: print(
f"{job['progress']['percent']:3}% {job['progress']['description']}"))
for job in client.job_updates(job_id): # same updates, as a loop
bar.update(job["progress"]["percent"])
upload(), download() and the _create/_update/_upsert helpers accept
the same callable as wait=.
job=True on any call does the same, as it does in the upstream TrueNAS
client: the call waits for the job it starts and returns the job's result.
client.api.pool.dataset.delete("tank/old", job=True) # returns once deleted
Without it the call returns the job id at once. The wait has no deadline unless
_timeout= is given.
Errors
Every error pytruenas raises at run time is a pytruenas.TrueNASError:
| Error | Raised when |
|---|---|
ClientException |
the middleware failed the call (.errno, .extra) |
ValidationErrors |
the server rejected the arguments, per field |
CallTimeout |
no answer within the timeout |
ConnectionClosed |
the connection dropped before the answer |
JobFailed |
a job ended FAILED or ABORTED |
ConnectError |
the websocket upgrade was refused (a wrong API path is a 404) |
AuthenticationError |
a login was refused |
APIDumpError |
the API definition was not fetched intact |
from pytruenas import TrueNASError, ValidationErrors
try:
client.api.user._create(username="svc")
except ValidationErrors as error:
print(error.errors) # [(attribute, message, errno), ...]
except TrueNASError as error:
print("TrueNAS refused:", error)
All of them are defined in pytruenas.exceptions. ValidationErrors is also
a ValueError. A mistake in the call itself — an unknown keyword, a malformed
target — is a plain TypeError or ValueError. A refused port, an
unresolvable host or a TLS error is the OSError it already is. No pytruenas
error is an OSError, so a timeout never reads as "not there" in a path's
exists().
Query filters
pytruenas.query provides operator wrappers so kwargs map to middleware
filter tuples:
from pytruenas import query as q
q.filter_from_kwargs(username="root", uid=q.GT(0))
# -> [("username", "=", "root"), ("uid", ">", 0)]
Available: EQ, NE, RE, GT, GE, LT, LE, IN, NIN.
See the Namespace API.